issue
adding a second OAuth account for the same integration, (e.g. gmail) silently replaced the first account's connection row instead of creating a new one. The connections list shows one connection
reproduce
- add a gmail integration
- add a connection for one gmail account (shows up correctly)
- add a second connection for another gmail account (does not show up)
cause
had a look into the root cause, the connection name is derived by connectionIdentifier, which turns a human label into a identifier-safe, camelCase name. On an OAuth connect the name passes through this function twice:
- client: derives the name from the label the user typed and sends it as the
name on POST /oauth/start.
- server:
mintOAuthConnection re-normalizes the incoming name with connectionIdentifier before writing the connection row
- in between,
oauth.start runs a free-name guard: when newConnection is set it calls connectionNameTaken and appends a 2 suffix if the name already exists, so a second connect mints fooBar2 instead of re-minting fooBar.
but connectionIdentifier was not idempotent, it lower-cased the entire input before tokenizing:
|
const words = input.toLowerCase().match(/[a-z0-9]+/g); |
Lower-casing first destroys camelCase word boundaries on a second pass, so
f(f(x)) !== f(x):
| input |
f(input) |
f(f(input)) |
stable? |
"Local Name" |
"localName" |
"localname" |
no |
"local name" |
"localName" |
"localname" |
no |
"localName" |
"localname" |
"localname" |
yes |
The client sent "localName"; the server re-normalized it to "localname".
The guard checked the raw name; the mint stored the re-normalized name. connectionNameTaken → findConnectionRow matches rows by an exact, case-sensitive SQL comparison on name. oauth.start fed it the raw client string:
|
let name = input.name; |
|
if (input.newConnection === true) { |
|
let suffix = 2; |
|
while ( |
|
yield* deps.connectionNameTaken({ |
|
owner: input.owner, |
|
integration: input.integration, |
|
name, |
|
}) |
|
) { |
|
if (suffix > 1000) { |
|
return yield* new OAuthStartError({ |
|
message: `No free connection name derivable from ${input.name}.`, |
|
}); |
|
} |
|
name = ConnectionName.make(`${String(input.name)}${suffix}`); |
|
suffix++; |
|
} |
So the guard compared "localName" against the stored row "localname", found no match, declared the name free, and skipped the suffix. The mint then re-normalized to "localname", matched the existing row, and issued an UPDATE — overwriting the first account's credentials, scopes, and label.
issue
adding a second OAuth account for the same integration, (e.g. gmail) silently replaced the first account's connection row instead of creating a new one. The connections list shows one connection
reproduce
cause
had a look into the root cause, the connection name is derived by
connectionIdentifier, which turns a human label into a identifier-safe, camelCase name. On an OAuth connect the name passes through this function twice:nameonPOST /oauth/start.mintOAuthConnectionre-normalizes the incoming name withconnectionIdentifierbefore writing theconnectionrowoauth.startruns a free-name guard: whennewConnectionis set it callsconnectionNameTakenand appends a2suffix if the name already exists, so a second connect mintsfooBar2instead of re-mintingfooBar.but
connectionIdentifierwas not idempotent, it lower-cased the entire input before tokenizing:executor/packages/core/sdk/src/connection-name-identifier.ts
Line 7 in 80e5530
Lower-casing first destroys camelCase word boundaries on a second pass, so
f(f(x)) !== f(x):f(input)f(f(input))"Local Name""localName""localname""local name""localName""localname""localName""localname""localname"The client sent
"localName"; the server re-normalized it to"localname".The guard checked the raw name; the mint stored the re-normalized name.
connectionNameTaken→findConnectionRowmatches rows by an exact, case-sensitive SQL comparison onname.oauth.startfed it the raw client string:executor/packages/core/sdk/src/oauth-service.ts
Lines 1081 to 1098 in 80e5530
So the guard compared
"localName"against the stored row"localname", found no match, declared the name free, and skipped the suffix. The mint then re-normalized to"localname", matched the existing row, and issued anUPDATE— overwriting the first account's credentials, scopes, and label.