@grantjs/schema 1.20.2 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grantjs/schema",
3
- "version": "1.20.2",
3
+ "version": "1.22.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1316,7 +1316,16 @@ export type ExportProjectUserCredentialsQueryVariables = Exact<{
1316
1316
 
1317
1317
  export type ExportProjectUserCredentialsQuery = {
1318
1318
  exportProjectUserCredentials: {
1319
- users: Array<{ userId: string; password: { algorithm: string; hash: string } | null }>;
1319
+ users: Array<{
1320
+ userId: string;
1321
+ password: { algorithm: string; hash: string } | null;
1322
+ apiKeySecrets: Array<{
1323
+ apiKeyId: string;
1324
+ clientId: string;
1325
+ fingerprint: string | null;
1326
+ secret: string | null;
1327
+ }> | null;
1328
+ }>;
1320
1329
  };
1321
1330
  };
1322
1331
 
@@ -1491,6 +1500,7 @@ export type ProjectSyncJobQuery = {
1491
1500
  passwordsUpdated: number;
1492
1501
  passwordsUnchanged: number;
1493
1502
  passwordsSkippedExisting: number;
1503
+ usersSkippedTombstoned: number;
1494
1504
  warnings: Array<string>;
1495
1505
  } | null;
1496
1506
  };
@@ -1547,6 +1557,18 @@ export type RequestProjectSyncUploadUrlMutation = {
1547
1557
  };
1548
1558
  };
1549
1559
 
1560
+ export type SetProjectExportableApiKeySecretsMutationVariables = Exact<{
1561
+ input: Types.SetProjectExportableApiKeySecretsInput;
1562
+ }>;
1563
+
1564
+ export type SetProjectExportableApiKeySecretsMutation = {
1565
+ setProjectExportableApiKeySecrets: {
1566
+ id: string;
1567
+ exportableApiKeySecrets: boolean;
1568
+ updatedAt: Date;
1569
+ };
1570
+ };
1571
+
1550
1572
  export type StartProjectExportMutationVariables = Exact<{
1551
1573
  id: string | number;
1552
1574
  scope: Types.Scope;
@@ -7545,6 +7567,19 @@ export const ExportProjectUserCredentialsDocument = {
7545
7567
  ],
7546
7568
  },
7547
7569
  },
7570
+ {
7571
+ kind: 'Field',
7572
+ name: { kind: 'Name', value: 'apiKeySecrets' },
7573
+ selectionSet: {
7574
+ kind: 'SelectionSet',
7575
+ selections: [
7576
+ { kind: 'Field', name: { kind: 'Name', value: 'apiKeyId' } },
7577
+ { kind: 'Field', name: { kind: 'Name', value: 'clientId' } },
7578
+ { kind: 'Field', name: { kind: 'Name', value: 'fingerprint' } },
7579
+ { kind: 'Field', name: { kind: 'Name', value: 'secret' } },
7580
+ ],
7581
+ },
7582
+ },
7548
7583
  ],
7549
7584
  },
7550
7585
  },
@@ -8165,6 +8200,7 @@ export const ProjectSyncJobDocument = {
8165
8200
  { kind: 'Field', name: { kind: 'Name', value: 'passwordsUpdated' } },
8166
8201
  { kind: 'Field', name: { kind: 'Name', value: 'passwordsUnchanged' } },
8167
8202
  { kind: 'Field', name: { kind: 'Name', value: 'passwordsSkippedExisting' } },
8203
+ { kind: 'Field', name: { kind: 'Name', value: 'usersSkippedTombstoned' } },
8168
8204
  { kind: 'Field', name: { kind: 'Name', value: 'warnings' } },
8169
8205
  ],
8170
8206
  },
@@ -8405,6 +8441,56 @@ export const RequestProjectSyncUploadUrlDocument = {
8405
8441
  RequestProjectSyncUploadUrlMutation,
8406
8442
  RequestProjectSyncUploadUrlMutationVariables
8407
8443
  >;
8444
+ export const SetProjectExportableApiKeySecretsDocument = {
8445
+ kind: 'Document',
8446
+ definitions: [
8447
+ {
8448
+ kind: 'OperationDefinition',
8449
+ operation: 'mutation',
8450
+ name: { kind: 'Name', value: 'SetProjectExportableApiKeySecrets' },
8451
+ variableDefinitions: [
8452
+ {
8453
+ kind: 'VariableDefinition',
8454
+ variable: { kind: 'Variable', name: { kind: 'Name', value: 'input' } },
8455
+ type: {
8456
+ kind: 'NonNullType',
8457
+ type: {
8458
+ kind: 'NamedType',
8459
+ name: { kind: 'Name', value: 'SetProjectExportableApiKeySecretsInput' },
8460
+ },
8461
+ },
8462
+ },
8463
+ ],
8464
+ selectionSet: {
8465
+ kind: 'SelectionSet',
8466
+ selections: [
8467
+ {
8468
+ kind: 'Field',
8469
+ name: { kind: 'Name', value: 'setProjectExportableApiKeySecrets' },
8470
+ arguments: [
8471
+ {
8472
+ kind: 'Argument',
8473
+ name: { kind: 'Name', value: 'input' },
8474
+ value: { kind: 'Variable', name: { kind: 'Name', value: 'input' } },
8475
+ },
8476
+ ],
8477
+ selectionSet: {
8478
+ kind: 'SelectionSet',
8479
+ selections: [
8480
+ { kind: 'Field', name: { kind: 'Name', value: 'id' } },
8481
+ { kind: 'Field', name: { kind: 'Name', value: 'exportableApiKeySecrets' } },
8482
+ { kind: 'Field', name: { kind: 'Name', value: 'updatedAt' } },
8483
+ ],
8484
+ },
8485
+ },
8486
+ ],
8487
+ },
8488
+ },
8489
+ ],
8490
+ } as unknown as DocumentNode<
8491
+ SetProjectExportableApiKeySecretsMutation,
8492
+ SetProjectExportableApiKeySecretsMutationVariables
8493
+ >;
8408
8494
  export const StartProjectExportDocument = {
8409
8495
  kind: 'Document',
8410
8496
  definitions: [
@@ -283,6 +283,7 @@ export type ResolversTypes = ResolversObject<{
283
283
  ExchangeApiKeyInput: Types.ExchangeApiKeyInput;
284
284
  ExchangeApiKeyResult: ResolverTypeWrapper<Types.ExchangeApiKeyResult>;
285
285
  ExportProjectUserCredentialsInput: Types.ExportProjectUserCredentialsInput;
286
+ ExportedApiKeySecret: ResolverTypeWrapper<Types.ExportedApiKeySecret>;
286
287
  ExportedPasswordCredential: ResolverTypeWrapper<Types.ExportedPasswordCredential>;
287
288
  ExportedProjectUserCredential: ResolverTypeWrapper<Types.ExportedProjectUserCredential>;
288
289
  GenerateMyMfaRecoveryCodesInput: Types.GenerateMyMfaRecoveryCodesInput;
@@ -389,6 +390,7 @@ export type ResolversTypes = ResolversObject<{
389
390
  ProjectTag: ResolverTypeWrapper<Types.ProjectTag>;
390
391
  ProjectUser: ResolverTypeWrapper<Types.ProjectUser>;
391
392
  ProjectUserApiKey: ResolverTypeWrapper<Types.ProjectUserApiKey>;
393
+ ProjectUserApiKeyCredential: ResolverTypeWrapper<Types.ProjectUserApiKeyCredential>;
392
394
  ProjectUserCredentialExport: ResolverTypeWrapper<Types.ProjectUserCredentialExport>;
393
395
  ProjectUserGroup: ResolverTypeWrapper<Types.ProjectUserGroup>;
394
396
  ProjectUserPasswordCredential: ResolverTypeWrapper<Types.ProjectUserPasswordCredential>;
@@ -507,6 +509,7 @@ export type ResolversTypes = ResolversObject<{
507
509
  SessionExportData: ResolverTypeWrapper<Types.SessionExportData>;
508
510
  SetMyNotificationPreferenceInput: Types.SetMyNotificationPreferenceInput;
509
511
  SetMyPrimaryMfaDeviceInput: Types.SetMyPrimaryMfaDeviceInput;
512
+ SetProjectExportableApiKeySecretsInput: Types.SetProjectExportableApiKeySecretsInput;
510
513
  SetProjectUserEmailInput: Types.SetProjectUserEmailInput;
511
514
  SetProjectUserPasswordInput: Types.SetProjectUserPasswordInput;
512
515
  SigningKey: ResolverTypeWrapper<Types.SigningKey>;
@@ -700,6 +703,7 @@ export type ResolversParentTypes = ResolversObject<{
700
703
  ExchangeApiKeyInput: Types.ExchangeApiKeyInput;
701
704
  ExchangeApiKeyResult: Types.ExchangeApiKeyResult;
702
705
  ExportProjectUserCredentialsInput: Types.ExportProjectUserCredentialsInput;
706
+ ExportedApiKeySecret: Types.ExportedApiKeySecret;
703
707
  ExportedPasswordCredential: Types.ExportedPasswordCredential;
704
708
  ExportedProjectUserCredential: Types.ExportedProjectUserCredential;
705
709
  GenerateMyMfaRecoveryCodesInput: Types.GenerateMyMfaRecoveryCodesInput;
@@ -782,6 +786,7 @@ export type ResolversParentTypes = ResolversObject<{
782
786
  ProjectTag: Types.ProjectTag;
783
787
  ProjectUser: Types.ProjectUser;
784
788
  ProjectUserApiKey: Types.ProjectUserApiKey;
789
+ ProjectUserApiKeyCredential: Types.ProjectUserApiKeyCredential;
785
790
  ProjectUserCredentialExport: Types.ProjectUserCredentialExport;
786
791
  ProjectUserGroup: Types.ProjectUserGroup;
787
792
  ProjectUserPasswordCredential: Types.ProjectUserPasswordCredential;
@@ -896,6 +901,7 @@ export type ResolversParentTypes = ResolversObject<{
896
901
  SessionExportData: Types.SessionExportData;
897
902
  SetMyNotificationPreferenceInput: Types.SetMyNotificationPreferenceInput;
898
903
  SetMyPrimaryMfaDeviceInput: Types.SetMyPrimaryMfaDeviceInput;
904
+ SetProjectExportableApiKeySecretsInput: Types.SetProjectExportableApiKeySecretsInput;
899
905
  SetProjectUserEmailInput: Types.SetProjectUserEmailInput;
900
906
  SetProjectUserPasswordInput: Types.SetProjectUserPasswordInput;
901
907
  SigningKey: Types.SigningKey;
@@ -1338,6 +1344,17 @@ export type ExchangeApiKeyResultResolvers<
1338
1344
  expiresIn?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
1339
1345
  }>;
1340
1346
 
1347
+ export type ExportedApiKeySecretResolvers<
1348
+ ContextType = any,
1349
+ ParentType extends ResolversParentTypes['ExportedApiKeySecret'] =
1350
+ ResolversParentTypes['ExportedApiKeySecret'],
1351
+ > = ResolversObject<{
1352
+ apiKeyId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
1353
+ clientId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
1354
+ fingerprint?: Resolver<Types.Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1355
+ secret?: Resolver<Types.Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1356
+ }>;
1357
+
1341
1358
  export type ExportedPasswordCredentialResolvers<
1342
1359
  ContextType = any,
1343
1360
  ParentType extends ResolversParentTypes['ExportedPasswordCredential'] =
@@ -1352,6 +1369,11 @@ export type ExportedProjectUserCredentialResolvers<
1352
1369
  ParentType extends ResolversParentTypes['ExportedProjectUserCredential'] =
1353
1370
  ResolversParentTypes['ExportedProjectUserCredential'],
1354
1371
  > = ResolversObject<{
1372
+ apiKeySecrets?: Resolver<
1373
+ Types.Maybe<Array<ResolversTypes['ExportedApiKeySecret']>>,
1374
+ ParentType,
1375
+ ContextType
1376
+ >;
1355
1377
  password?: Resolver<
1356
1378
  Types.Maybe<ResolversTypes['ExportedPasswordCredential']>,
1357
1379
  ParentType,
@@ -1997,6 +2019,12 @@ export type MutationResolvers<
1997
2019
  ContextType,
1998
2020
  RequireFields<Types.MutationSetMyPrimaryMfaDeviceArgs, 'input'>
1999
2021
  >;
2022
+ setProjectExportableApiKeySecrets?: Resolver<
2023
+ ResolversTypes['Project'],
2024
+ ParentType,
2025
+ ContextType,
2026
+ RequireFields<Types.MutationSetProjectExportableApiKeySecretsArgs, 'input'>
2027
+ >;
2000
2028
  setProjectUserEmail?: Resolver<
2001
2029
  ResolversTypes['User'],
2002
2030
  ParentType,
@@ -2577,6 +2605,7 @@ export type ProjectResolvers<
2577
2605
  createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
2578
2606
  deletedAt?: Resolver<Types.Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
2579
2607
  description?: Resolver<Types.Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2608
+ exportableApiKeySecrets?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
2580
2609
  groups?: Resolver<Types.Maybe<Array<ResolversTypes['Group']>>, ParentType, ContextType>;
2581
2610
  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
2582
2611
  name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2887,6 +2916,18 @@ export type ProjectUserApiKeyResolvers<
2887
2916
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2888
2917
  }>;
2889
2918
 
2919
+ export type ProjectUserApiKeyCredentialResolvers<
2920
+ ContextType = any,
2921
+ ParentType extends ResolversParentTypes['ProjectUserApiKeyCredential'] =
2922
+ ResolversParentTypes['ProjectUserApiKeyCredential'],
2923
+ > = ResolversObject<{
2924
+ apiKeyId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
2925
+ clientId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2926
+ exportable?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
2927
+ fingerprint?: Resolver<Types.Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2928
+ updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
2929
+ }>;
2930
+
2890
2931
  export type ProjectUserCredentialExportResolvers<
2891
2932
  ContextType = any,
2892
2933
  ParentType extends ResolversParentTypes['ProjectUserCredentialExport'] =
@@ -3404,6 +3445,7 @@ export type SyncProjectResultResolvers<
3404
3445
  userRolesAssigned?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3405
3446
  userTagsLinked?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3406
3447
  usersCreated?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3448
+ usersSkippedTombstoned?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3407
3449
  warnings?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
3408
3450
  }>;
3409
3451
 
@@ -3500,6 +3542,11 @@ export type UserResolvers<
3500
3542
  ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'],
3501
3543
  > = ResolversObject<{
3502
3544
  accounts?: Resolver<Types.Maybe<Array<ResolversTypes['Account']>>, ParentType, ContextType>;
3545
+ apiKeyCredentials?: Resolver<
3546
+ Types.Maybe<Array<ResolversTypes['ProjectUserApiKeyCredential']>>,
3547
+ ParentType,
3548
+ ContextType
3549
+ >;
3503
3550
  authenticationMethods?: Resolver<
3504
3551
  Types.Maybe<Array<ResolversTypes['UserAuthenticationMethod']>>,
3505
3552
  ParentType,
@@ -3836,6 +3883,7 @@ export type Resolvers<ContextType = any> = ResolversObject<{
3836
3883
  CredentialExportData?: CredentialExportDataResolvers<ContextType>;
3837
3884
  Date?: GraphQLScalarType;
3838
3885
  ExchangeApiKeyResult?: ExchangeApiKeyResultResolvers<ContextType>;
3886
+ ExportedApiKeySecret?: ExportedApiKeySecretResolvers<ContextType>;
3839
3887
  ExportedPasswordCredential?: ExportedPasswordCredentialResolvers<ContextType>;
3840
3888
  ExportedProjectUserCredential?: ExportedProjectUserCredentialResolvers<ContextType>;
3841
3889
  Group?: GroupResolvers<ContextType>;
@@ -3896,6 +3944,7 @@ export type Resolvers<ContextType = any> = ResolversObject<{
3896
3944
  ProjectTag?: ProjectTagResolvers<ContextType>;
3897
3945
  ProjectUser?: ProjectUserResolvers<ContextType>;
3898
3946
  ProjectUserApiKey?: ProjectUserApiKeyResolvers<ContextType>;
3947
+ ProjectUserApiKeyCredential?: ProjectUserApiKeyCredentialResolvers<ContextType>;
3899
3948
  ProjectUserCredentialExport?: ProjectUserCredentialExportResolvers<ContextType>;
3900
3949
  ProjectUserGroup?: ProjectUserGroupResolvers<ContextType>;
3901
3950
  ProjectUserPasswordCredential?: ProjectUserPasswordCredentialResolvers<ContextType>;
@@ -453,6 +453,11 @@ export type CdmKeyResolverInput = {
453
453
  * import/export.
454
454
  */
455
455
  findBy?: InputMaybe<CdmFindBy>;
456
+ /**
457
+ * Only with `findBy: key`: an existing, live Grant user this key is, reused instead of creating one and marked
458
+ * with the key for this project. Refused for a missing, deleted or already-imported-under-another-key user.
459
+ */
460
+ grantUserId?: InputMaybe<Scalars['ID']['input']>;
456
461
  ifMissing?: InputMaybe<CdmIfMissing>;
457
462
  /**
458
463
  * Identifier value. For `findBy: email`, Grant normalizes this to lowercase
@@ -813,6 +818,11 @@ export type CredentialExportData = {
813
818
  * not as a second export.
814
819
  */
815
820
  export enum CredentialKind {
821
+ /**
822
+ * The API keys of the project user, with the plaintext secret of each key the project keeps an
823
+ * exportable copy of (a project that opted in to exportable API-key secrets).
824
+ */
825
+ ApiKeySecret = 'API_KEY_SECRET',
816
826
  /** The project password credential (bcrypt hash). */
817
827
  Password = 'PASSWORD',
818
828
  }
@@ -871,6 +881,26 @@ export type ExportProjectUserCredentialsInput = {
871
881
  userIds: Array<Scalars['ID']['input']>;
872
882
  };
873
883
 
884
+ /** One API key of a project user in an export. `secret` is a credential: handle it like one. */
885
+ export type ExportedApiKeySecret = {
886
+ __typename?: 'ExportedApiKeySecret';
887
+ apiKeyId: Scalars['ID']['output'];
888
+ /** The key's public client id. */
889
+ clientId: Scalars['String']['output'];
890
+ /**
891
+ * Lowercase hex SHA-256 of the secret string as issued, to compare with `sha256(secret)` of the
892
+ * value a receiver holds. `null` for a key created before the fingerprint existed and not rotated
893
+ * since.
894
+ */
895
+ fingerprint?: Maybe<Scalars['String']['output']>;
896
+ /**
897
+ * The plaintext secret: 88 characters, standard base64 of 64 random bytes. `null` for a key the
898
+ * project holds no copy of (created before the project opted in, created by a CDM import, or
899
+ * rotated by a CDM replace): Grant cannot recover it.
900
+ */
901
+ secret?: Maybe<Scalars['String']['output']>;
902
+ };
903
+
874
904
  /** A stored password hash in CDM shape. Secret-equivalent: handle like a credential. */
875
905
  export type ExportedPasswordCredential = {
876
906
  __typename?: 'ExportedPasswordCredential';
@@ -882,7 +912,12 @@ export type ExportedPasswordCredential = {
882
912
 
883
913
  export type ExportedProjectUserCredential = {
884
914
  __typename?: 'ExportedProjectUserCredential';
885
- /** `null` when the member has no project password credential. */
915
+ /**
916
+ * The member's live (not revoked, not deleted) project-user API keys, oldest first. `null` when the
917
+ * `API_KEY_SECRET` kind was not requested; `[]` for a member with no key.
918
+ */
919
+ apiKeySecrets?: Maybe<Array<ExportedApiKeySecret>>;
920
+ /** `null` when the member has no project password credential, and when the `PASSWORD` kind was not requested. */
886
921
  password?: Maybe<ExportedPasswordCredential>;
887
922
  userId: Scalars['ID']['output'];
888
923
  };
@@ -1324,6 +1359,15 @@ export type Mutation = {
1324
1359
  setMyNotificationPreference: NotificationPreference;
1325
1360
  setMyPrimaryAuthenticationMethod: UserAuthenticationMethod;
1326
1361
  setMyPrimaryMfaDevice: MfaDevice;
1362
+ /**
1363
+ * Opt a project in or out of keeping an exportable, encrypted copy of a project user's API-key
1364
+ * secret. Project scopes only. Requires `Project:SetExportableApiKeySecrets`, held by the Owner and
1365
+ * Admin roles and not by Dev. Enabling fails with a configuration error while
1366
+ * `API_KEY_EXPORTABLE_SECRET_ENCRYPTION_KEY` is not set; disabling never needs it. Each change is
1367
+ * audited on the project with the old and new value; the same value is a no-op. Nothing is
1368
+ * back-filled: keys that exist before the opt-in keep only their hash.
1369
+ */
1370
+ setProjectExportableApiKeySecrets: Project;
1327
1371
  /**
1328
1372
  * Set or replace a project member's Email authentication method as unverified.
1329
1373
  * `project.user_email_changed` carries ids only, never the address.
@@ -1699,6 +1743,10 @@ export type MutationSetMyPrimaryMfaDeviceArgs = {
1699
1743
  input: SetMyPrimaryMfaDeviceInput;
1700
1744
  };
1701
1745
 
1746
+ export type MutationSetProjectExportableApiKeySecretsArgs = {
1747
+ input: SetProjectExportableApiKeySecretsInput;
1748
+ };
1749
+
1702
1750
  export type MutationSetProjectUserEmailArgs = {
1703
1751
  input: SetProjectUserEmailInput;
1704
1752
  };
@@ -2236,6 +2284,13 @@ export type Project = Auditable & {
2236
2284
  createdAt: Scalars['Date']['output'];
2237
2285
  deletedAt?: Maybe<Scalars['Date']['output']>;
2238
2286
  description?: Maybe<Scalars['String']['output']>;
2287
+ /**
2288
+ * Whether this project keeps an exportable, encrypted copy of a project user's API-key secret at
2289
+ * create and rotate. Off by default; set with `setProjectExportableApiKeySecrets`. A plain
2290
+ * configuration flag: it says nothing about any key, and nothing is back-filled for keys that
2291
+ * already exist.
2292
+ */
2293
+ exportableApiKeySecrets: Scalars['Boolean']['output'];
2239
2294
  groups?: Maybe<Array<Group>>;
2240
2295
  id: Scalars['ID']['output'];
2241
2296
  name: Scalars['String']['output'];
@@ -2580,7 +2635,33 @@ export type ProjectUserApiKey = Auditable & {
2580
2635
  userId: Scalars['ID']['output'];
2581
2636
  };
2582
2637
 
2583
- /** Result of `exportProjectUserCredentials`. This is the one place a credential hash leaves Grant. */
2638
+ /**
2639
+ * Non-secret facts about one of a project user's API keys. Never contains the secret, the hash or the
2640
+ * encrypted copy.
2641
+ */
2642
+ export type ProjectUserApiKeyCredential = {
2643
+ __typename?: 'ProjectUserApiKeyCredential';
2644
+ apiKeyId: Scalars['ID']['output'];
2645
+ /** The key's public client id. */
2646
+ clientId: Scalars['String']['output'];
2647
+ /** An encrypted copy of the key's current secret exists (only in a project that opted in). */
2648
+ exportable: Scalars['Boolean']['output'];
2649
+ /**
2650
+ * Lowercase hex SHA-256 of the secret string as issued (the text the client received, never the
2651
+ * stored hash): a change detector a holder of the secret can compare against. `null` for a key
2652
+ * created before this field existed and not rotated since. Grant issues secrets of 32 or 64 random
2653
+ * bytes, so the digest cannot be guessed offline; a secret chosen by an importer is only as strong
2654
+ * as the importer made it.
2655
+ */
2656
+ fingerprint?: Maybe<Scalars['String']['output']>;
2657
+ /** When the key last changed (created, rotated or edited). */
2658
+ updatedAt: Scalars['Date']['output'];
2659
+ };
2660
+
2661
+ /**
2662
+ * Result of `exportProjectUserCredentials`. This is the one place a credential hash, and the plaintext
2663
+ * secret of an exportable API key, leaves Grant.
2664
+ */
2584
2665
  export type ProjectUserCredentialExport = {
2585
2666
  __typename?: 'ProjectUserCredentialExport';
2586
2667
  users: Array<ExportedProjectUserCredential>;
@@ -2629,11 +2710,14 @@ export type Query = {
2629
2710
  /** Public signup policy. No authentication required. */
2630
2711
  authSignupPolicy: AuthSignupPolicy;
2631
2712
  /**
2632
- * Export project members' password hashes in CDM shape, for a downstream identity mirror.
2633
- * This is the one audited exception to "APIs expose only whether a password exists": it
2634
- * requires `ProjectUser:ExportCredentials` (held by no standard group), every call is audited,
2635
- * and it returns hashes. Any non-member in `userIds` fails the whole call; members without a
2636
- * project credential are listed with `password: null`.
2713
+ * Export project members' credentials for a downstream identity mirror: password hashes in CDM
2714
+ * shape (`PASSWORD`) and the plaintext secrets of their API keys (`API_KEY_SECRET`, only for keys
2715
+ * of a project that opted in to exportable API-key secrets). This is the one audited exception to
2716
+ * "APIs expose only whether a password exists" and to "an API-key secret is shown once": it
2717
+ * requires `ProjectUser:ExportCredentials` (held by no standard group), every call is audited
2718
+ * (one summary row, plus one row per exported API-key secret), and it returns credentials. Any
2719
+ * non-member in `userIds` fails the whole call; members without a project credential are listed
2720
+ * with `password: null`, and a key without a copy with `secret: null`.
2637
2721
  */
2638
2722
  exportProjectUserCredentials: ProjectUserCredentialExport;
2639
2723
  groups: GroupPage;
@@ -3633,6 +3717,19 @@ export type SetMyPrimaryMfaDeviceInput = {
3633
3717
  factorId: Scalars['ID']['input'];
3634
3718
  };
3635
3719
 
3720
+ export type SetProjectExportableApiKeySecretsInput = {
3721
+ /**
3722
+ * `true` opts the project in; `false` opts it out and deletes every exportable copy the project
3723
+ * holds. Setting the value the project already has changes nothing and is not audited.
3724
+ */
3725
+ enabled: Scalars['Boolean']['input'];
3726
+ /**
3727
+ * The project to change, as the scope the request is authorized in: `accountProject` or
3728
+ * `organizationProject` only.
3729
+ */
3730
+ scope: Scope;
3731
+ };
3732
+
3636
3733
  export type SetProjectUserEmailInput = {
3637
3734
  /**
3638
3735
  * Unverified Email authentication method. Project-scope only.
@@ -3797,6 +3894,11 @@ export type SyncProjectResult = {
3797
3894
  userRolesAssigned: Scalars['Int']['output'];
3798
3895
  userTagsLinked: Scalars['Int']['output'];
3799
3896
  usersCreated: Scalars['Int']['output'];
3897
+ /**
3898
+ * Document users not created because the person deleted this imported identity; their
3899
+ * membership, credential and API keys were not written either.
3900
+ */
3901
+ usersSkippedTombstoned: Scalars['Int']['output'];
3800
3902
  warnings: Array<Scalars['String']['output']>;
3801
3903
  };
3802
3904
 
@@ -4217,6 +4319,13 @@ export type UpsertProjectOAuthConnectionInput = {
4217
4319
  export type User = Auditable & {
4218
4320
  __typename?: 'User';
4219
4321
  accounts?: Maybe<Array<Account>>;
4322
+ /**
4323
+ * Non-secret facts about the user's live (not revoked, not deleted) project-user API keys in the
4324
+ * request's project scope, oldest first. `null` outside a project scope or when the caller lacks
4325
+ * `ProjectUser:ExportCredentials` (never an error); `[]` for a member with no key. The secret, the
4326
+ * hash and the encrypted copy are never returned here.
4327
+ */
4328
+ apiKeyCredentials?: Maybe<Array<ProjectUserApiKeyCredential>>;
4220
4329
  authenticationMethods?: Maybe<Array<UserAuthenticationMethod>>;
4221
4330
  createdAt: Scalars['Date']['output'];
4222
4331
  deletedAt?: Maybe<Scalars['Date']['output']>;
@@ -6,6 +6,12 @@ query ExportProjectUserCredentials($input: ExportProjectUserCredentialsInput!) {
6
6
  algorithm
7
7
  hash
8
8
  }
9
+ apiKeySecrets {
10
+ apiKeyId
11
+ clientId
12
+ fingerprint
13
+ secret
14
+ }
9
15
  }
10
16
  }
11
17
  }
@@ -29,6 +29,7 @@ query ProjectSyncJob($id: ID!, $scope: Scope!, $jobId: ID!) {
29
29
  passwordsUpdated
30
30
  passwordsUnchanged
31
31
  passwordsSkippedExisting
32
+ usersSkippedTombstoned
32
33
  warnings
33
34
  }
34
35
  warnings
@@ -0,0 +1,7 @@
1
+ mutation SetProjectExportableApiKeySecrets($input: SetProjectExportableApiKeySecretsInput!) {
2
+ setProjectExportableApiKeySecrets(input: $input) {
3
+ id
4
+ exportableApiKeySecrets
5
+ updatedAt
6
+ }
7
+ }
@@ -1,10 +1,13 @@
1
1
  extend type Query {
2
2
  """
3
- Export project members' password hashes in CDM shape, for a downstream identity mirror.
4
- This is the one audited exception to "APIs expose only whether a password exists": it
5
- requires `ProjectUser:ExportCredentials` (held by no standard group), every call is audited,
6
- and it returns hashes. Any non-member in `userIds` fails the whole call; members without a
7
- project credential are listed with `password: null`.
3
+ Export project members' credentials for a downstream identity mirror: password hashes in CDM
4
+ shape (`PASSWORD`) and the plaintext secrets of their API keys (`API_KEY_SECRET`, only for keys
5
+ of a project that opted in to exportable API-key secrets). This is the one audited exception to
6
+ "APIs expose only whether a password exists" and to "an API-key secret is shown once": it
7
+ requires `ProjectUser:ExportCredentials` (held by no standard group), every call is audited
8
+ (one summary row, plus one row per exported API-key secret), and it returns credentials. Any
9
+ non-member in `userIds` fails the whole call; members without a project credential are listed
10
+ with `password: null`, and a key without a copy with `secret: null`.
8
11
  """
9
12
  exportProjectUserCredentials(
10
13
  input: ExportProjectUserCredentialsInput!
@@ -5,4 +5,9 @@ not as a second export.
5
5
  enum CredentialKind {
6
6
  "The project password credential (bcrypt hash)."
7
7
  PASSWORD
8
+ """
9
+ The API keys of the project user, with the plaintext secret of each key the project keeps an
10
+ exportable copy of (a project that opted in to exportable API-key secrets).
11
+ """
12
+ API_KEY_SECRET
8
13
  }
@@ -0,0 +1,21 @@
1
+ """
2
+ Non-secret facts about one of a project user's API keys. Never contains the secret, the hash or the
3
+ encrypted copy.
4
+ """
5
+ type ProjectUserApiKeyCredential {
6
+ apiKeyId: ID!
7
+ "The key's public client id."
8
+ clientId: String!
9
+ """
10
+ Lowercase hex SHA-256 of the secret string as issued (the text the client received, never the
11
+ stored hash): a change detector a holder of the secret can compare against. `null` for a key
12
+ created before this field existed and not rotated since. Grant issues secrets of 32 or 64 random
13
+ bytes, so the digest cannot be guessed offline; a secret chosen by an importer is only as strong
14
+ as the importer made it.
15
+ """
16
+ fingerprint: String
17
+ "An encrypted copy of the key's current secret exists (only in a project that opted in)."
18
+ exportable: Boolean!
19
+ "When the key last changed (created, rotated or edited)."
20
+ updatedAt: Date!
21
+ }
@@ -1,5 +1,6 @@
1
1
  """
2
- Result of `exportProjectUserCredentials`. This is the one place a credential hash leaves Grant.
2
+ Result of `exportProjectUserCredentials`. This is the one place a credential hash, and the plaintext
3
+ secret of an exportable API key, leaves Grant.
3
4
  """
4
5
  type ProjectUserCredentialExport {
5
6
  users: [ExportedProjectUserCredential!]!
@@ -7,8 +8,34 @@ type ProjectUserCredentialExport {
7
8
 
8
9
  type ExportedProjectUserCredential {
9
10
  userId: ID!
10
- "`null` when the member has no project password credential."
11
+ "`null` when the member has no project password credential, and when the `PASSWORD` kind was not requested."
11
12
  password: ExportedPasswordCredential
13
+ """
14
+ The member's live (not revoked, not deleted) project-user API keys, oldest first. `null` when the
15
+ `API_KEY_SECRET` kind was not requested; `[]` for a member with no key.
16
+ """
17
+ apiKeySecrets: [ExportedApiKeySecret!]
18
+ }
19
+
20
+ """
21
+ One API key of a project user in an export. `secret` is a credential: handle it like one.
22
+ """
23
+ type ExportedApiKeySecret {
24
+ apiKeyId: ID!
25
+ "The key's public client id."
26
+ clientId: String!
27
+ """
28
+ Lowercase hex SHA-256 of the secret string as issued, to compare with `sha256(secret)` of the
29
+ value a receiver holds. `null` for a key created before the fingerprint existed and not rotated
30
+ since.
31
+ """
32
+ fingerprint: String
33
+ """
34
+ The plaintext secret: 88 characters, standard base64 of 64 random bytes. `null` for a key the
35
+ project holds no copy of (created before the project opted in, created by a CDM import, or
36
+ rotated by a CDM replace): Grant cannot recover it.
37
+ """
38
+ secret: String
12
39
  }
13
40
 
14
41
  """
@@ -0,0 +1,12 @@
1
+ input SetProjectExportableApiKeySecretsInput {
2
+ """
3
+ The project to change, as the scope the request is authorized in: `accountProject` or
4
+ `organizationProject` only.
5
+ """
6
+ scope: Scope!
7
+ """
8
+ `true` opts the project in; `false` opts it out and deletes every exportable copy the project
9
+ holds. Setting the value the project already has changes nothing and is not audited.
10
+ """
11
+ enabled: Boolean!
12
+ }
@@ -53,6 +53,11 @@ input CdmKeyResolverInput {
53
53
  """
54
54
  findBy: CdmFindBy
55
55
  ifMissing: CdmIfMissing
56
+ """
57
+ Only with `findBy: key`: an existing, live Grant user this key is, reused instead of creating one and marked
58
+ with the key for this project. Refused for a missing, deleted or already-imported-under-another-key user.
59
+ """
60
+ grantUserId: ID
56
61
  }
57
62
 
58
63
  """
@@ -0,0 +1,11 @@
1
+ extend type Mutation {
2
+ """
3
+ Opt a project in or out of keeping an exportable, encrypted copy of a project user's API-key
4
+ secret. Project scopes only. Requires `Project:SetExportableApiKeySecrets`, held by the Owner and
5
+ Admin roles and not by Dev. Enabling fails with a configuration error while
6
+ `API_KEY_EXPORTABLE_SECRET_ENCRYPTION_KEY` is not set; disabling never needs it. Each change is
7
+ audited on the project with the old and new value; the same value is a no-op. Nothing is
8
+ back-filled: keys that exist before the opt-in keep only their hash.
9
+ """
10
+ setProjectExportableApiKeySecrets(input: SetProjectExportableApiKeySecretsInput!): Project!
11
+ }
@@ -12,6 +12,13 @@ type Project implements Auditable {
12
12
  Whether the OAuth help panel is shown. Null uses Grant's default (shown).
13
13
  """
14
14
  showHelpPanel: Boolean
15
+ """
16
+ Whether this project keeps an exportable, encrypted copy of a project user's API-key secret at
17
+ create and rotate. Off by default; set with `setProjectExportableApiKeySecrets`. A plain
18
+ configuration flag: it says nothing about any key, and nothing is back-filled for keys that
19
+ already exist.
20
+ """
21
+ exportableApiKeySecrets: Boolean!
15
22
  roles: [Role!]
16
23
  groups: [Group!]
17
24
  permissions: [Permission!]
@@ -51,6 +51,11 @@ type SyncProjectResult {
51
51
  reset in Grant, not imported.
52
52
  """
53
53
  passwordsSkippedExisting: Int!
54
+ """
55
+ Document users not created because the person deleted this imported identity; their
56
+ membership, credential and API keys were not written either.
57
+ """
58
+ usersSkippedTombstoned: Int!
54
59
  resourcesCreated: Int!
55
60
  permissionsCreated: Int!
56
61
  tagsCreated: Int!
@@ -21,6 +21,13 @@ type User implements Auditable {
21
21
  stored hash string: a change detector, not a secret. The password and the hash are never returned here.
22
22
  """
23
23
  passwordCredential: ProjectUserPasswordCredential
24
+ """
25
+ Non-secret facts about the user's live (not revoked, not deleted) project-user API keys in the
26
+ request's project scope, oldest first. `null` outside a project scope or when the caller lacks
27
+ `ProjectUser:ExportCredentials` (never an error); `[]` for a member with no key. The secret, the
28
+ hash and the encrypted copy are never returned here.
29
+ """
30
+ apiKeyCredentials: [ProjectUserApiKeyCredential!]
24
31
  createdAt: Date!
25
32
  updatedAt: Date!
26
33
  deletedAt: Date
@@ -53,10 +53,11 @@ describe('operation documents', () => {
53
53
  // 137 after CDM imports by reference: RequestProjectSyncUploadUrlDocument.
54
54
  // 138 after operator-set-email: SetProjectUserEmailDocument.
55
55
  // 140 after operator-set-password: SetProjectUserPasswordDocument, ExportProjectUserCredentialsDocument.
56
+ // 141 after exportable-api-key-secret opt-in: SetProjectExportableApiKeySecretsDocument.
56
57
  // Updating this number is the point of the pin, not a nuisance: every change
57
58
  // to it should be a line in a commit message.
58
59
  it('finds every document under src/operations', () => {
59
- expect(files).toHaveLength(140);
60
+ expect(files).toHaveLength(141);
60
61
  });
61
62
 
62
63
  it.each(files.map((f) => [relative(process.cwd(), f), f]))('%s validates', (_label, file) => {