@grantjs/schema 1.21.0 → 1.22.1

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.21.0",
3
+ "version": "1.22.1",
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
 
@@ -1548,6 +1557,18 @@ export type RequestProjectSyncUploadUrlMutation = {
1548
1557
  };
1549
1558
  };
1550
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
+
1551
1572
  export type StartProjectExportMutationVariables = Exact<{
1552
1573
  id: string | number;
1553
1574
  scope: Types.Scope;
@@ -7546,6 +7567,19 @@ export const ExportProjectUserCredentialsDocument = {
7546
7567
  ],
7547
7568
  },
7548
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
+ },
7549
7583
  ],
7550
7584
  },
7551
7585
  },
@@ -8407,6 +8441,56 @@ export const RequestProjectSyncUploadUrlDocument = {
8407
8441
  RequestProjectSyncUploadUrlMutation,
8408
8442
  RequestProjectSyncUploadUrlMutationVariables
8409
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
+ >;
8410
8494
  export const StartProjectExportDocument = {
8411
8495
  kind: 'Document',
8412
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'] =
@@ -3501,6 +3542,11 @@ export type UserResolvers<
3501
3542
  ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'],
3502
3543
  > = ResolversObject<{
3503
3544
  accounts?: Resolver<Types.Maybe<Array<ResolversTypes['Account']>>, ParentType, ContextType>;
3545
+ apiKeyCredentials?: Resolver<
3546
+ Types.Maybe<Array<ResolversTypes['ProjectUserApiKeyCredential']>>,
3547
+ ParentType,
3548
+ ContextType
3549
+ >;
3504
3550
  authenticationMethods?: Resolver<
3505
3551
  Types.Maybe<Array<ResolversTypes['UserAuthenticationMethod']>>,
3506
3552
  ParentType,
@@ -3837,6 +3883,7 @@ export type Resolvers<ContextType = any> = ResolversObject<{
3837
3883
  CredentialExportData?: CredentialExportDataResolvers<ContextType>;
3838
3884
  Date?: GraphQLScalarType;
3839
3885
  ExchangeApiKeyResult?: ExchangeApiKeyResultResolvers<ContextType>;
3886
+ ExportedApiKeySecret?: ExportedApiKeySecretResolvers<ContextType>;
3840
3887
  ExportedPasswordCredential?: ExportedPasswordCredentialResolvers<ContextType>;
3841
3888
  ExportedProjectUserCredential?: ExportedProjectUserCredentialResolvers<ContextType>;
3842
3889
  Group?: GroupResolvers<ContextType>;
@@ -3897,6 +3944,7 @@ export type Resolvers<ContextType = any> = ResolversObject<{
3897
3944
  ProjectTag?: ProjectTagResolvers<ContextType>;
3898
3945
  ProjectUser?: ProjectUserResolvers<ContextType>;
3899
3946
  ProjectUserApiKey?: ProjectUserApiKeyResolvers<ContextType>;
3947
+ ProjectUserApiKeyCredential?: ProjectUserApiKeyCredentialResolvers<ContextType>;
3900
3948
  ProjectUserCredentialExport?: ProjectUserCredentialExportResolvers<ContextType>;
3901
3949
  ProjectUserGroup?: ProjectUserGroupResolvers<ContextType>;
3902
3950
  ProjectUserPasswordCredential?: ProjectUserPasswordCredentialResolvers<ContextType>;
@@ -818,6 +818,11 @@ export type CredentialExportData = {
818
818
  * not as a second export.
819
819
  */
820
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',
821
826
  /** The project password credential (bcrypt hash). */
822
827
  Password = 'PASSWORD',
823
828
  }
@@ -876,6 +881,26 @@ export type ExportProjectUserCredentialsInput = {
876
881
  userIds: Array<Scalars['ID']['input']>;
877
882
  };
878
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
+
879
904
  /** A stored password hash in CDM shape. Secret-equivalent: handle like a credential. */
880
905
  export type ExportedPasswordCredential = {
881
906
  __typename?: 'ExportedPasswordCredential';
@@ -887,7 +912,12 @@ export type ExportedPasswordCredential = {
887
912
 
888
913
  export type ExportedProjectUserCredential = {
889
914
  __typename?: 'ExportedProjectUserCredential';
890
- /** `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. */
891
921
  password?: Maybe<ExportedPasswordCredential>;
892
922
  userId: Scalars['ID']['output'];
893
923
  };
@@ -1329,6 +1359,15 @@ export type Mutation = {
1329
1359
  setMyNotificationPreference: NotificationPreference;
1330
1360
  setMyPrimaryAuthenticationMethod: UserAuthenticationMethod;
1331
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;
1332
1371
  /**
1333
1372
  * Set or replace a project member's Email authentication method as unverified.
1334
1373
  * `project.user_email_changed` carries ids only, never the address.
@@ -1704,6 +1743,10 @@ export type MutationSetMyPrimaryMfaDeviceArgs = {
1704
1743
  input: SetMyPrimaryMfaDeviceInput;
1705
1744
  };
1706
1745
 
1746
+ export type MutationSetProjectExportableApiKeySecretsArgs = {
1747
+ input: SetProjectExportableApiKeySecretsInput;
1748
+ };
1749
+
1707
1750
  export type MutationSetProjectUserEmailArgs = {
1708
1751
  input: SetProjectUserEmailInput;
1709
1752
  };
@@ -2241,6 +2284,13 @@ export type Project = Auditable & {
2241
2284
  createdAt: Scalars['Date']['output'];
2242
2285
  deletedAt?: Maybe<Scalars['Date']['output']>;
2243
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'];
2244
2294
  groups?: Maybe<Array<Group>>;
2245
2295
  id: Scalars['ID']['output'];
2246
2296
  name: Scalars['String']['output'];
@@ -2585,7 +2635,33 @@ export type ProjectUserApiKey = Auditable & {
2585
2635
  userId: Scalars['ID']['output'];
2586
2636
  };
2587
2637
 
2588
- /** 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
+ */
2589
2665
  export type ProjectUserCredentialExport = {
2590
2666
  __typename?: 'ProjectUserCredentialExport';
2591
2667
  users: Array<ExportedProjectUserCredential>;
@@ -2634,11 +2710,14 @@ export type Query = {
2634
2710
  /** Public signup policy. No authentication required. */
2635
2711
  authSignupPolicy: AuthSignupPolicy;
2636
2712
  /**
2637
- * Export project members' password hashes in CDM shape, for a downstream identity mirror.
2638
- * This is the one audited exception to "APIs expose only whether a password exists": it
2639
- * requires `ProjectUser:ExportCredentials` (held by no standard group), every call is audited,
2640
- * and it returns hashes. Any non-member in `userIds` fails the whole call; members without a
2641
- * 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`.
2642
2721
  */
2643
2722
  exportProjectUserCredentials: ProjectUserCredentialExport;
2644
2723
  groups: GroupPage;
@@ -3638,6 +3717,19 @@ export type SetMyPrimaryMfaDeviceInput = {
3638
3717
  factorId: Scalars['ID']['input'];
3639
3718
  };
3640
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
+
3641
3733
  export type SetProjectUserEmailInput = {
3642
3734
  /**
3643
3735
  * Unverified Email authentication method. Project-scope only.
@@ -4227,6 +4319,13 @@ export type UpsertProjectOAuthConnectionInput = {
4227
4319
  export type User = Auditable & {
4228
4320
  __typename?: 'User';
4229
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>>;
4230
4329
  authenticationMethods?: Maybe<Array<UserAuthenticationMethod>>;
4231
4330
  createdAt: Scalars['Date']['output'];
4232
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
  }
@@ -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
+ }
@@ -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!]
@@ -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) => {