@grantjs/schema 1.14.3 → 1.16.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.14.3",
3
+ "version": "1.16.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1465,6 +1465,10 @@ export type ProjectSyncJobQuery = {
1465
1465
  usersCreated: number;
1466
1466
  userRolesAssigned: number;
1467
1467
  projectUserApiKeysCreated: number;
1468
+ passwordsImported: number;
1469
+ passwordsUpdated: number;
1470
+ passwordsUnchanged: number;
1471
+ passwordsSkippedExisting: number;
1468
1472
  warnings: Array<string>;
1469
1473
  } | null;
1470
1474
  };
@@ -1505,6 +1509,22 @@ export type ProjectSyncJobsQuery = {
1505
1509
  };
1506
1510
  };
1507
1511
 
1512
+ export type RequestProjectSyncUploadUrlMutationVariables = Exact<{
1513
+ id: string | number;
1514
+ scope: Types.Scope;
1515
+ input: Types.RequestProjectSyncUploadUrlInput;
1516
+ }>;
1517
+
1518
+ export type RequestProjectSyncUploadUrlMutation = {
1519
+ requestProjectSyncUploadUrl: {
1520
+ uploadId: string;
1521
+ url: string;
1522
+ method: string;
1523
+ expiresAt: Date;
1524
+ headers: Array<{ name: string; value: string }>;
1525
+ };
1526
+ };
1527
+
1508
1528
  export type StartProjectExportMutationVariables = Exact<{
1509
1529
  id: string | number;
1510
1530
  scope: Types.Scope;
@@ -8001,6 +8021,10 @@ export const ProjectSyncJobDocument = {
8001
8021
  { kind: 'Field', name: { kind: 'Name', value: 'usersCreated' } },
8002
8022
  { kind: 'Field', name: { kind: 'Name', value: 'userRolesAssigned' } },
8003
8023
  { kind: 'Field', name: { kind: 'Name', value: 'projectUserApiKeysCreated' } },
8024
+ { kind: 'Field', name: { kind: 'Name', value: 'passwordsImported' } },
8025
+ { kind: 'Field', name: { kind: 'Name', value: 'passwordsUpdated' } },
8026
+ { kind: 'Field', name: { kind: 'Name', value: 'passwordsUnchanged' } },
8027
+ { kind: 'Field', name: { kind: 'Name', value: 'passwordsSkippedExisting' } },
8004
8028
  { kind: 'Field', name: { kind: 'Name', value: 'warnings' } },
8005
8029
  ],
8006
8030
  },
@@ -8153,6 +8177,94 @@ export const ProjectSyncJobsDocument = {
8153
8177
  },
8154
8178
  ],
8155
8179
  } as unknown as DocumentNode<ProjectSyncJobsQuery, ProjectSyncJobsQueryVariables>;
8180
+ export const RequestProjectSyncUploadUrlDocument = {
8181
+ kind: 'Document',
8182
+ definitions: [
8183
+ {
8184
+ kind: 'OperationDefinition',
8185
+ operation: 'mutation',
8186
+ name: { kind: 'Name', value: 'RequestProjectSyncUploadUrl' },
8187
+ variableDefinitions: [
8188
+ {
8189
+ kind: 'VariableDefinition',
8190
+ variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
8191
+ type: {
8192
+ kind: 'NonNullType',
8193
+ type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
8194
+ },
8195
+ },
8196
+ {
8197
+ kind: 'VariableDefinition',
8198
+ variable: { kind: 'Variable', name: { kind: 'Name', value: 'scope' } },
8199
+ type: {
8200
+ kind: 'NonNullType',
8201
+ type: { kind: 'NamedType', name: { kind: 'Name', value: 'Scope' } },
8202
+ },
8203
+ },
8204
+ {
8205
+ kind: 'VariableDefinition',
8206
+ variable: { kind: 'Variable', name: { kind: 'Name', value: 'input' } },
8207
+ type: {
8208
+ kind: 'NonNullType',
8209
+ type: {
8210
+ kind: 'NamedType',
8211
+ name: { kind: 'Name', value: 'RequestProjectSyncUploadUrlInput' },
8212
+ },
8213
+ },
8214
+ },
8215
+ ],
8216
+ selectionSet: {
8217
+ kind: 'SelectionSet',
8218
+ selections: [
8219
+ {
8220
+ kind: 'Field',
8221
+ name: { kind: 'Name', value: 'requestProjectSyncUploadUrl' },
8222
+ arguments: [
8223
+ {
8224
+ kind: 'Argument',
8225
+ name: { kind: 'Name', value: 'id' },
8226
+ value: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
8227
+ },
8228
+ {
8229
+ kind: 'Argument',
8230
+ name: { kind: 'Name', value: 'scope' },
8231
+ value: { kind: 'Variable', name: { kind: 'Name', value: 'scope' } },
8232
+ },
8233
+ {
8234
+ kind: 'Argument',
8235
+ name: { kind: 'Name', value: 'input' },
8236
+ value: { kind: 'Variable', name: { kind: 'Name', value: 'input' } },
8237
+ },
8238
+ ],
8239
+ selectionSet: {
8240
+ kind: 'SelectionSet',
8241
+ selections: [
8242
+ { kind: 'Field', name: { kind: 'Name', value: 'uploadId' } },
8243
+ { kind: 'Field', name: { kind: 'Name', value: 'url' } },
8244
+ { kind: 'Field', name: { kind: 'Name', value: 'method' } },
8245
+ { kind: 'Field', name: { kind: 'Name', value: 'expiresAt' } },
8246
+ {
8247
+ kind: 'Field',
8248
+ name: { kind: 'Name', value: 'headers' },
8249
+ selectionSet: {
8250
+ kind: 'SelectionSet',
8251
+ selections: [
8252
+ { kind: 'Field', name: { kind: 'Name', value: 'name' } },
8253
+ { kind: 'Field', name: { kind: 'Name', value: 'value' } },
8254
+ ],
8255
+ },
8256
+ },
8257
+ ],
8258
+ },
8259
+ },
8260
+ ],
8261
+ },
8262
+ },
8263
+ ],
8264
+ } as unknown as DocumentNode<
8265
+ RequestProjectSyncUploadUrlMutation,
8266
+ RequestProjectSyncUploadUrlMutationVariables
8267
+ >;
8156
8268
  export const StartProjectExportDocument = {
8157
8269
  kind: 'Document',
8158
8270
  definitions: [
@@ -237,6 +237,8 @@ export type ResolversTypes = ResolversObject<{
237
237
  CdmModeInput: Types.CdmModeInput;
238
238
  CdmModeStrategy: Types.CdmModeStrategy;
239
239
  CdmOnConflict: Types.CdmOnConflict;
240
+ CdmPasswordAlgorithm: Types.CdmPasswordAlgorithm;
241
+ CdmPasswordInput: Types.CdmPasswordInput;
240
242
  ChangeMyPasswordInput: Types.ChangeMyPasswordInput;
241
243
  ChangeMyPasswordResult: ResolverTypeWrapper<Types.ChangeMyPasswordResult>;
242
244
  ClearProjectAppPictureInput: Types.ClearProjectAppPictureInput;
@@ -378,6 +380,7 @@ export type ResolversTypes = ResolversObject<{
378
380
  ProjectSyncJobSortInput: Types.ProjectSyncJobSortInput;
379
381
  ProjectSyncJobSortableField: Types.ProjectSyncJobSortableField;
380
382
  ProjectSyncJobStatus: Types.ProjectSyncJobStatus;
383
+ ProjectSyncUploadUrl: ResolverTypeWrapper<Types.ProjectSyncUploadUrl>;
381
384
  ProjectTag: ResolverTypeWrapper<Types.ProjectTag>;
382
385
  ProjectUser: ResolverTypeWrapper<Types.ProjectUser>;
383
386
  ProjectUserApiKey: ResolverTypeWrapper<Types.ProjectUserApiKey>;
@@ -464,6 +467,7 @@ export type ResolversTypes = ResolversObject<{
464
467
  RequestPasswordResetResponse: ResolverTypeWrapper<Types.RequestPasswordResetResponse>;
465
468
  RequestProjectAppPictureUploadUrlInput: Types.RequestProjectAppPictureUploadUrlInput;
466
469
  RequestProjectPictureUploadUrlInput: Types.RequestProjectPictureUploadUrlInput;
470
+ RequestProjectSyncUploadUrlInput: Types.RequestProjectSyncUploadUrlInput;
467
471
  RequestUserPictureUploadUrlInput: Types.RequestUserPictureUploadUrlInput;
468
472
  ResendVerificationInput: Types.ResendVerificationInput;
469
473
  ResendVerificationResponse: ResolverTypeWrapper<Types.ResendVerificationResponse>;
@@ -502,6 +506,7 @@ export type ResolversTypes = ResolversObject<{
502
506
  String: ResolverTypeWrapper<Types.Scalars['String']['output']>;
503
507
  SyncProjectInput: Types.SyncProjectInput;
504
508
  SyncProjectResult: ResolverTypeWrapper<Types.SyncProjectResult>;
509
+ SyncProjectSourceInput: Types.SyncProjectSourceInput;
505
510
  Tag: ResolverTypeWrapper<Types.Tag>;
506
511
  TagCdmInput: Types.TagCdmInput;
507
512
  TagPage: ResolverTypeWrapper<Types.TagPage>;
@@ -643,6 +648,7 @@ export type ResolversParentTypes = ResolversObject<{
643
648
  Boolean: Types.Scalars['Boolean']['output'];
644
649
  CdmKeyResolverInput: Types.CdmKeyResolverInput;
645
650
  CdmModeInput: Types.CdmModeInput;
651
+ CdmPasswordInput: Types.CdmPasswordInput;
646
652
  ChangeMyPasswordInput: Types.ChangeMyPasswordInput;
647
653
  ChangeMyPasswordResult: Types.ChangeMyPasswordResult;
648
654
  ClearProjectAppPictureInput: Types.ClearProjectAppPictureInput;
@@ -759,6 +765,7 @@ export type ResolversParentTypes = ResolversObject<{
759
765
  ProjectSyncJob: Types.ProjectSyncJob;
760
766
  ProjectSyncJobPage: Types.ProjectSyncJobPage;
761
767
  ProjectSyncJobSortInput: Types.ProjectSyncJobSortInput;
768
+ ProjectSyncUploadUrl: Types.ProjectSyncUploadUrl;
762
769
  ProjectTag: Types.ProjectTag;
763
770
  ProjectUser: Types.ProjectUser;
764
771
  ProjectUserApiKey: Types.ProjectUserApiKey;
@@ -845,6 +852,7 @@ export type ResolversParentTypes = ResolversObject<{
845
852
  RequestPasswordResetResponse: Types.RequestPasswordResetResponse;
846
853
  RequestProjectAppPictureUploadUrlInput: Types.RequestProjectAppPictureUploadUrlInput;
847
854
  RequestProjectPictureUploadUrlInput: Types.RequestProjectPictureUploadUrlInput;
855
+ RequestProjectSyncUploadUrlInput: Types.RequestProjectSyncUploadUrlInput;
848
856
  RequestUserPictureUploadUrlInput: Types.RequestUserPictureUploadUrlInput;
849
857
  ResendVerificationInput: Types.ResendVerificationInput;
850
858
  ResendVerificationResponse: Types.ResendVerificationResponse;
@@ -878,6 +886,7 @@ export type ResolversParentTypes = ResolversObject<{
878
886
  String: Types.Scalars['String']['output'];
879
887
  SyncProjectInput: Types.SyncProjectInput;
880
888
  SyncProjectResult: Types.SyncProjectResult;
889
+ SyncProjectSourceInput: Types.SyncProjectSourceInput;
881
890
  Tag: Types.Tag;
882
891
  TagCdmInput: Types.TagCdmInput;
883
892
  TagPage: Types.TagPage;
@@ -1839,6 +1848,12 @@ export type MutationResolvers<
1839
1848
  ContextType,
1840
1849
  RequireFields<Types.MutationRequestProjectPictureUploadUrlArgs, 'input'>
1841
1850
  >;
1851
+ requestProjectSyncUploadUrl?: Resolver<
1852
+ ResolversTypes['ProjectSyncUploadUrl'],
1853
+ ParentType,
1854
+ ContextType,
1855
+ RequireFields<Types.MutationRequestProjectSyncUploadUrlArgs, 'id' | 'input' | 'scope'>
1856
+ >;
1842
1857
  requestUserPictureUploadUrl?: Resolver<
1843
1858
  ResolversTypes['UploadUrl'],
1844
1859
  ParentType,
@@ -2733,6 +2748,18 @@ export type ProjectSyncJobPageResolvers<
2733
2748
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2734
2749
  }>;
2735
2750
 
2751
+ export type ProjectSyncUploadUrlResolvers<
2752
+ ContextType = any,
2753
+ ParentType extends ResolversParentTypes['ProjectSyncUploadUrl'] =
2754
+ ResolversParentTypes['ProjectSyncUploadUrl'],
2755
+ > = ResolversObject<{
2756
+ expiresAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
2757
+ headers?: Resolver<Array<ResolversTypes['UploadUrlHeader']>, ParentType, ContextType>;
2758
+ method?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2759
+ uploadId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
2760
+ url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2761
+ }>;
2762
+
2736
2763
  export type ProjectTagResolvers<
2737
2764
  ContextType = any,
2738
2765
  ParentType extends ResolversParentTypes['ProjectTag'] = ResolversParentTypes['ProjectTag'],
@@ -3264,6 +3291,10 @@ export type SyncProjectResultResolvers<
3264
3291
  groupTagsLinked?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3265
3292
  groupsCreated?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3266
3293
  importId?: Resolver<Types.Maybe<ResolversTypes['String']>, ParentType, ContextType>;
3294
+ passwordsImported?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3295
+ passwordsSkippedExisting?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3296
+ passwordsUnchanged?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3297
+ passwordsUpdated?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3267
3298
  permissionsCreated?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3268
3299
  projectGroupsLinked?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
3269
3300
  projectId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
@@ -3760,6 +3791,7 @@ export type Resolvers<ContextType = any> = ResolversObject<{
3760
3791
  ProjectRolePermission?: ProjectRolePermissionResolvers<ContextType>;
3761
3792
  ProjectSyncJob?: ProjectSyncJobResolvers<ContextType>;
3762
3793
  ProjectSyncJobPage?: ProjectSyncJobPageResolvers<ContextType>;
3794
+ ProjectSyncUploadUrl?: ProjectSyncUploadUrlResolvers<ContextType>;
3763
3795
  ProjectTag?: ProjectTagResolvers<ContextType>;
3764
3796
  ProjectUser?: ProjectUserResolvers<ContextType>;
3765
3797
  ProjectUserApiKey?: ProjectUserApiKeyResolvers<ContextType>;
@@ -448,8 +448,9 @@ export enum CdmIfMissing {
448
448
  export type CdmKeyResolverInput = {
449
449
  /**
450
450
  * `id` references an existing Grant user id. `email` resolves or creates a
451
- * global passwordless email authentication method. Other values are treated as
452
- * CDM-local external keys for project import/export.
451
+ * global email authentication method, passwordless unless the user carries a
452
+ * `password`. Other values are treated as CDM-local external keys for project
453
+ * import/export.
453
454
  */
454
455
  findBy?: InputMaybe<CdmFindBy>;
455
456
  ifMissing?: InputMaybe<CdmIfMissing>;
@@ -479,6 +480,31 @@ export enum CdmOnConflict {
479
480
  Update = 'update',
480
481
  }
481
482
 
483
+ /**
484
+ * Password hash algorithms a CDM import can carry. Only `bcrypt` is accepted; the
485
+ * enum leaves room for further algorithms without changing the input shape.
486
+ */
487
+ export enum CdmPasswordAlgorithm {
488
+ Bcrypt = 'bcrypt',
489
+ }
490
+
491
+ /**
492
+ * A password hash the importer already holds, to be stored as the user's email
493
+ * password credential (ADR 0010). Never the plaintext password.
494
+ */
495
+ export type CdmPasswordInput = {
496
+ algorithm: CdmPasswordAlgorithm;
497
+ /**
498
+ * A bcrypt hash in the modular-crypt form `$2a$`, `$2b$` or `$2y$`, a two-digit
499
+ * cost and 53 salt-and-hash characters (60 in all), such as the output of Go's
500
+ * `bcrypt.GenerateFromPassword`. The cost must be within the range the deployment
501
+ * accepts (4 to 15). It is written only to the credential store: it is not kept in
502
+ * metadata, searchable tokens, audit values, events or the stored job payload once
503
+ * the job finishes, and an invalid hash is refused without being echoed.
504
+ */
505
+ hash: Scalars['String']['input'];
506
+ };
507
+
482
508
  export type ChangeMyPasswordInput = {
483
509
  confirmPassword: Scalars['String']['input'];
484
510
  /** Required when a password already exists; omit when setting the first password. */
@@ -1191,6 +1217,19 @@ export type Mutation = {
1191
1217
  * is recorded against the project.
1192
1218
  */
1193
1219
  requestProjectPictureUploadUrl: UploadUrl;
1220
+ /**
1221
+ * Ask for a URL to upload a CDM import document directly to storage, so it can be
1222
+ * imported without passing through this API's request body (and its size limit).
1223
+ *
1224
+ * Requires Project sync job update permission in the given project scope, like
1225
+ * `startProjectSync`. The server derives the storage key from the project and a
1226
+ * server-minted `uploadId`; the client never supplies a path. The URL commits to exactly
1227
+ * `input.contentLength` bytes of `application/json`, up to `CDM_UPLOAD_MAX_BYTES`.
1228
+ *
1229
+ * PUT the file, then call `startProjectSync` with `input.source.uploadId`. An upload
1230
+ * that is never started is expired by the storage lifecycle rule.
1231
+ */
1232
+ requestProjectSyncUploadUrl: ProjectSyncUploadUrl;
1194
1233
  /**
1195
1234
  * Ask for a URL to upload another user's picture directly to storage.
1196
1235
  *
@@ -1519,6 +1558,12 @@ export type MutationRequestProjectPictureUploadUrlArgs = {
1519
1558
  input: RequestProjectPictureUploadUrlInput;
1520
1559
  };
1521
1560
 
1561
+ export type MutationRequestProjectSyncUploadUrlArgs = {
1562
+ id: Scalars['ID']['input'];
1563
+ input: RequestProjectSyncUploadUrlInput;
1564
+ scope: Scope;
1565
+ };
1566
+
1522
1567
  export type MutationRequestUserPictureUploadUrlArgs = {
1523
1568
  input: RequestUserPictureUploadUrlInput;
1524
1569
  };
@@ -2382,6 +2427,28 @@ export enum ProjectSyncJobStatus {
2382
2427
  Running = 'RUNNING',
2383
2428
  }
2384
2429
 
2430
+ /**
2431
+ * A bounded, time-limited URL to PUT a CDM import document to, and the id to start the
2432
+ * import from. Same commitments as `UploadUrl`: exactly `contentLength` bytes of exactly
2433
+ * `contentType`, at a path derived on the server from the project, until `expiresAt`.
2434
+ */
2435
+ export type ProjectSyncUploadUrl = {
2436
+ __typename?: 'ProjectSyncUploadUrl';
2437
+ /** Instant after which the store refuses the URL. */
2438
+ expiresAt: Scalars['Date']['output'];
2439
+ /** Headers the client must set verbatim. `Content-Length` is deliberately absent. */
2440
+ headers: Array<UploadUrlHeader>;
2441
+ /** HTTP method the client must use. Always PUT today. */
2442
+ method: Scalars['String']['output'];
2443
+ /** Pass as `source.uploadId` to `startProjectSync`. */
2444
+ uploadId: Scalars['ID']['output'];
2445
+ /**
2446
+ * Absolute for object stores; application-relative for targets this API serves
2447
+ * itself. Resolve it against the API origin before using it.
2448
+ */
2449
+ url: Scalars['String']['output'];
2450
+ };
2451
+
2385
2452
  export type ProjectTag = Auditable & {
2386
2453
  __typename?: 'ProjectTag';
2387
2454
  createdAt: Scalars['Date']['output'];
@@ -3144,6 +3211,16 @@ export type RequestProjectPictureUploadUrlInput = {
3144
3211
  scope: Scope;
3145
3212
  };
3146
3213
 
3214
+ export type RequestProjectSyncUploadUrlInput = {
3215
+ /**
3216
+ * Exact byte length of the CDM JSON file the client will PUT. The URL commits to it,
3217
+ * and it may not exceed `CDM_UPLOAD_MAX_BYTES` (default 64 MiB).
3218
+ */
3219
+ contentLength: Scalars['Int']['input'];
3220
+ /** Defaults to `application/json`, the only type accepted. */
3221
+ contentType?: InputMaybe<Scalars['String']['input']>;
3222
+ };
3223
+
3147
3224
  export type RequestUserPictureUploadUrlInput = {
3148
3225
  /**
3149
3226
  * Exact byte length the client will send. Validated against the upload policy
@@ -3499,6 +3576,13 @@ export type StartProjectExportInput = {
3499
3576
  /**
3500
3577
  * Versioned canonical project document (CDM) for export/import: roles, users,
3501
3578
  * resources, permissions, groups, and tags scoped to one project.
3579
+ *
3580
+ * Two shapes. **Inline**: `version`, `roles` and `users` are required and the document is
3581
+ * the input. **By reference**: `source` is set, `mode` still says how to apply it, and
3582
+ * `version` is optional and the entity arrays (`roles`, `users`, `resources`,
3583
+ * `permissions`, `groups`, `tags`) must be absent: the document is the file uploaded
3584
+ * through `requestProjectSyncUploadUrl`, and the worker validates it, so a schema error or
3585
+ * an over-limit entity count surfaces as a `FAILED` job, not as an error on the mutation.
3502
3586
  */
3503
3587
  export type SyncProjectInput = {
3504
3588
  groups?: InputMaybe<Array<GroupCdmInput>>;
@@ -3506,10 +3590,19 @@ export type SyncProjectInput = {
3506
3590
  mode: CdmModeInput;
3507
3591
  permissions?: InputMaybe<Array<PermissionCdmInput>>;
3508
3592
  resources?: InputMaybe<Array<ResourceCdmInput>>;
3509
- roles: Array<RoleCdmInput>;
3593
+ /** Required inline; must be absent with `source`. */
3594
+ roles?: InputMaybe<Array<RoleCdmInput>>;
3595
+ /**
3596
+ * Import the document uploaded through `requestProjectSyncUploadUrl` instead of sending
3597
+ * it inline. The entity arrays must then be absent, and `mode` (and `version`, if
3598
+ * given) must match the uploaded document.
3599
+ */
3600
+ source?: InputMaybe<SyncProjectSourceInput>;
3510
3601
  tags?: InputMaybe<Array<TagCdmInput>>;
3511
- users: Array<UserCdmInput>;
3512
- version: Scalars['Int']['input'];
3602
+ /** Required inline; must be absent with `source`. */
3603
+ users?: InputMaybe<Array<UserCdmInput>>;
3604
+ /** Required inline; optional with `source`, where the uploaded document carries it. */
3605
+ version?: InputMaybe<Scalars['Int']['input']>;
3513
3606
  };
3514
3607
 
3515
3608
  /** Counters and warnings from a completed CDM import. */
@@ -3519,6 +3612,22 @@ export type SyncProjectResult = {
3519
3612
  groupTagsLinked: Scalars['Int']['output'];
3520
3613
  groupsCreated: Scalars['Int']['output'];
3521
3614
  importId?: Maybe<Scalars['String']['output']>;
3615
+ /** Users whose `password` hash was stored as their email credential for the first time. */
3616
+ passwordsImported: Scalars['Int']['output'];
3617
+ /**
3618
+ * Users whose `password` hash was not stored because the credential or the identity is
3619
+ * not the import's to write: a password set in Grant, an address the owner has verified,
3620
+ * or an identity that already existed and was not created by an earlier import of this
3621
+ * project.
3622
+ */
3623
+ passwordsSkippedExisting: Scalars['Int']['output'];
3624
+ /** Users whose imported credential already equalled the hash in the document. */
3625
+ passwordsUnchanged: Scalars['Int']['output'];
3626
+ /**
3627
+ * Users whose imported credential was replaced because the document carried a different
3628
+ * hash for it.
3629
+ */
3630
+ passwordsUpdated: Scalars['Int']['output'];
3522
3631
  permissionsCreated: Scalars['Int']['output'];
3523
3632
  projectGroupsLinked: Scalars['Int']['output'];
3524
3633
  projectId: Scalars['ID']['output'];
@@ -3539,6 +3648,24 @@ export type SyncProjectResult = {
3539
3648
  warnings: Array<Scalars['String']['output']>;
3540
3649
  };
3541
3650
 
3651
+ /**
3652
+ * Where the CDM document of a by-reference import is: an upload made through
3653
+ * `requestProjectSyncUploadUrl`. See `SyncProjectInput.source`.
3654
+ */
3655
+ export type SyncProjectSourceInput = {
3656
+ /**
3657
+ * How many entities (roles, users, resources, permissions, groups and tags) the
3658
+ * uploaded document holds. A hint, recorded for the job list and never trusted: the
3659
+ * worker counts the document itself.
3660
+ */
3661
+ entityCount?: InputMaybe<Scalars['Int']['input']>;
3662
+ /**
3663
+ * The `uploadId` returned by `requestProjectSyncUploadUrl`. The server derives the
3664
+ * storage key from the project and this id; the client never supplies a path.
3665
+ */
3666
+ uploadId: Scalars['ID']['input'];
3667
+ };
3668
+
3542
3669
  export type Tag = Auditable & {
3543
3670
  __typename?: 'Tag';
3544
3671
  color: Scalars['String']['output'];
@@ -4004,6 +4131,22 @@ export type UserCdmInput = {
4004
4131
  key: CdmKeyResolverInput;
4005
4132
  metadata?: InputMaybe<Scalars['JSON']['input']>;
4006
4133
  name: Scalars['String']['input'];
4134
+ /**
4135
+ * Existing password credential to import for this user. Only valid with
4136
+ * `key.findBy: email`, because the credential lives on the email authentication
4137
+ * method; any other resolver is a validation error.
4138
+ *
4139
+ * Ownership is forward-only. An imported credential is marked as imported and stays
4140
+ * owned by the importing system while the email is unverified: a later import of this
4141
+ * project with a different hash replaces it, an identical one changes nothing. Once the
4142
+ * owner verifies the address, or the password is set, changed or reset in Grant, Grant owns
4143
+ * it and imports never touch it again. A credential is written only for an identity the
4144
+ * import creates, or one an earlier import of this project created whose email is still
4145
+ * unverified; any other existing identity, including one found through GitHub or Google,
4146
+ * is left untouched. An absent `password` never removes a
4147
+ * stored credential. See `SyncProjectResult` for the counters.
4148
+ */
4149
+ password?: InputMaybe<CdmPasswordInput>;
4007
4150
  /** Direct user → permission grants (document keys or catalog `resourceSlug:action` refs). */
4008
4151
  permissions?: InputMaybe<Array<Scalars['String']['input']>>;
4009
4152
  primaryTag?: InputMaybe<Scalars['String']['input']>;
@@ -22,6 +22,10 @@ query ProjectSyncJob($id: ID!, $scope: Scope!, $jobId: ID!) {
22
22
  usersCreated
23
23
  userRolesAssigned
24
24
  projectUserApiKeysCreated
25
+ passwordsImported
26
+ passwordsUpdated
27
+ passwordsUnchanged
28
+ passwordsSkippedExisting
25
29
  warnings
26
30
  }
27
31
  warnings
@@ -0,0 +1,16 @@
1
+ mutation RequestProjectSyncUploadUrl(
2
+ $id: ID!
3
+ $scope: Scope!
4
+ $input: RequestProjectSyncUploadUrlInput!
5
+ ) {
6
+ requestProjectSyncUploadUrl(id: $id, scope: $scope, input: $input) {
7
+ uploadId
8
+ url
9
+ method
10
+ expiresAt
11
+ headers {
12
+ name
13
+ value
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ input RequestProjectSyncUploadUrlInput {
2
+ """
3
+ Exact byte length of the CDM JSON file the client will PUT. The URL commits to it,
4
+ and it may not exceed `CDM_UPLOAD_MAX_BYTES` (default 64 MiB).
5
+ """
6
+ contentLength: Int!
7
+ "Defaults to `application/json`, the only type accepted."
8
+ contentType: String
9
+ }
@@ -23,6 +23,14 @@ enum CdmIfMissing {
23
23
  createNew
24
24
  }
25
25
 
26
+ """
27
+ Password hash algorithms a CDM import can carry. Only `bcrypt` is accepted; the
28
+ enum leaves room for further algorithms without changing the input shape.
29
+ """
30
+ enum CdmPasswordAlgorithm {
31
+ bcrypt
32
+ }
33
+
26
34
  input CdmModeInput {
27
35
  strategy: CdmModeStrategy!
28
36
  onConflict: CdmOnConflict
@@ -39,8 +47,9 @@ input CdmKeyResolverInput {
39
47
  value: String!
40
48
  """
41
49
  `id` references an existing Grant user id. `email` resolves or creates a
42
- global passwordless email authentication method. Other values are treated as
43
- CDM-local external keys for project import/export.
50
+ global email authentication method, passwordless unless the user carries a
51
+ `password`. Other values are treated as CDM-local external keys for project
52
+ import/export.
44
53
  """
45
54
  findBy: CdmFindBy
46
55
  ifMissing: CdmIfMissing
@@ -128,6 +137,23 @@ input UserApiKeyCdmInput {
128
137
  metadata: JSON
129
138
  }
130
139
 
140
+ """
141
+ A password hash the importer already holds, to be stored as the user's email
142
+ password credential (ADR 0010). Never the plaintext password.
143
+ """
144
+ input CdmPasswordInput {
145
+ algorithm: CdmPasswordAlgorithm!
146
+ """
147
+ A bcrypt hash in the modular-crypt form `$2a$`, `$2b$` or `$2y$`, a two-digit
148
+ cost and 53 salt-and-hash characters (60 in all), such as the output of Go's
149
+ `bcrypt.GenerateFromPassword`. The cost must be within the range the deployment
150
+ accepts (4 to 15). It is written only to the credential store: it is not kept in
151
+ metadata, searchable tokens, audit values, events or the stored job payload once
152
+ the job finishes, and an invalid hash is refused without being echoed.
153
+ """
154
+ hash: String!
155
+ }
156
+
131
157
  input UserCdmInput {
132
158
  key: CdmKeyResolverInput!
133
159
  name: String!
@@ -147,24 +173,74 @@ input UserCdmInput {
147
173
  primaryTag: String
148
174
  apiKeys: [UserApiKeyCdmInput!]
149
175
  """
176
+ Existing password credential to import for this user. Only valid with
177
+ `key.findBy: email`, because the credential lives on the email authentication
178
+ method; any other resolver is a validation error.
179
+
180
+ Ownership is forward-only. An imported credential is marked as imported and stays
181
+ owned by the importing system while the email is unverified: a later import of this
182
+ project with a different hash replaces it, an identical one changes nothing. Once the
183
+ owner verifies the address, or the password is set, changed or reset in Grant, Grant owns
184
+ it and imports never touch it again. A credential is written only for an identity the
185
+ import creates, or one an earlier import of this project created whose email is still
186
+ unverified; any other existing identity, including one found through GitHub or Google,
187
+ is left untouched. An absent `password` never removes a
188
+ stored credential. See `SyncProjectResult` for the counters.
189
+ """
190
+ password: CdmPasswordInput
191
+ """
150
192
  Denormalized search tokens for project-scoped user list views.
151
193
  """
152
194
  searchable: JSON
153
195
  metadata: JSON
154
196
  }
155
197
 
198
+ """
199
+ Where the CDM document of a by-reference import is: an upload made through
200
+ `requestProjectSyncUploadUrl`. See `SyncProjectInput.source`.
201
+ """
202
+ input SyncProjectSourceInput {
203
+ """
204
+ The `uploadId` returned by `requestProjectSyncUploadUrl`. The server derives the
205
+ storage key from the project and this id; the client never supplies a path.
206
+ """
207
+ uploadId: ID!
208
+ """
209
+ How many entities (roles, users, resources, permissions, groups and tags) the
210
+ uploaded document holds. A hint, recorded for the job list and never trusted: the
211
+ worker counts the document itself.
212
+ """
213
+ entityCount: Int
214
+ }
215
+
156
216
  """
157
217
  Versioned canonical project document (CDM) for export/import: roles, users,
158
218
  resources, permissions, groups, and tags scoped to one project.
219
+
220
+ Two shapes. **Inline**: `version`, `roles` and `users` are required and the document is
221
+ the input. **By reference**: `source` is set, `mode` still says how to apply it, and
222
+ `version` is optional and the entity arrays (`roles`, `users`, `resources`,
223
+ `permissions`, `groups`, `tags`) must be absent: the document is the file uploaded
224
+ through `requestProjectSyncUploadUrl`, and the worker validates it, so a schema error or
225
+ an over-limit entity count surfaces as a `FAILED` job, not as an error on the mutation.
159
226
  """
160
227
  input SyncProjectInput {
161
- version: Int!
228
+ "Required inline; optional with `source`, where the uploaded document carries it."
229
+ version: Int
162
230
  id: String
163
231
  mode: CdmModeInput!
164
- roles: [RoleCdmInput!]!
165
- users: [UserCdmInput!]!
232
+ "Required inline; must be absent with `source`."
233
+ roles: [RoleCdmInput!]
234
+ "Required inline; must be absent with `source`."
235
+ users: [UserCdmInput!]
166
236
  resources: [ResourceCdmInput!]
167
237
  permissions: [PermissionCdmInput!]
168
238
  groups: [GroupCdmInput!]
169
239
  tags: [TagCdmInput!]
240
+ """
241
+ Import the document uploaded through `requestProjectSyncUploadUrl` instead of sending
242
+ it inline. The entity arrays must then be absent, and `mode` (and `version`, if
243
+ given) must match the uploaded document.
244
+ """
245
+ source: SyncProjectSourceInput
170
246
  }
@@ -0,0 +1,19 @@
1
+ extend type Mutation {
2
+ """
3
+ Ask for a URL to upload a CDM import document directly to storage, so it can be
4
+ imported without passing through this API's request body (and its size limit).
5
+
6
+ Requires Project sync job update permission in the given project scope, like
7
+ `startProjectSync`. The server derives the storage key from the project and a
8
+ server-minted `uploadId`; the client never supplies a path. The URL commits to exactly
9
+ `input.contentLength` bytes of `application/json`, up to `CDM_UPLOAD_MAX_BYTES`.
10
+
11
+ PUT the file, then call `startProjectSync` with `input.source.uploadId`. An upload
12
+ that is never started is expired by the storage lifecycle rule.
13
+ """
14
+ requestProjectSyncUploadUrl(
15
+ id: ID!
16
+ scope: Scope!
17
+ input: RequestProjectSyncUploadUrlInput!
18
+ ): ProjectSyncUploadUrl!
19
+ }
@@ -0,0 +1,24 @@
1
+ """
2
+ A bounded, time-limited URL to PUT a CDM import document to, and the id to start the
3
+ import from. Same commitments as `UploadUrl`: exactly `contentLength` bytes of exactly
4
+ `contentType`, at a path derived on the server from the project, until `expiresAt`.
5
+ """
6
+ type ProjectSyncUploadUrl {
7
+ "Pass as `source.uploadId` to `startProjectSync`."
8
+ uploadId: ID!
9
+
10
+ """
11
+ Absolute for object stores; application-relative for targets this API serves
12
+ itself. Resolve it against the API origin before using it.
13
+ """
14
+ url: String!
15
+
16
+ "HTTP method the client must use. Always PUT today."
17
+ method: String!
18
+
19
+ "Headers the client must set verbatim. `Content-Length` is deliberately absent."
20
+ headers: [UploadUrlHeader!]!
21
+
22
+ "Instant after which the store refuses the URL."
23
+ expiresAt: Date!
24
+ }
@@ -16,6 +16,26 @@ type SyncProjectResult {
16
16
  usersCreated: Int!
17
17
  userRolesAssigned: Int!
18
18
  projectUserApiKeysCreated: Int!
19
+ """
20
+ Users whose `password` hash was stored as their email credential for the first time.
21
+ """
22
+ passwordsImported: Int!
23
+ """
24
+ Users whose imported credential was replaced because the document carried a different
25
+ hash for it.
26
+ """
27
+ passwordsUpdated: Int!
28
+ """
29
+ Users whose imported credential already equalled the hash in the document.
30
+ """
31
+ passwordsUnchanged: Int!
32
+ """
33
+ Users whose `password` hash was not stored because the credential or the identity is
34
+ not the import's to write: a password set in Grant, an address the owner has verified,
35
+ or an identity that already existed and was not created by an earlier import of this
36
+ project.
37
+ """
38
+ passwordsSkippedExisting: Int!
19
39
  resourcesCreated: Int!
20
40
  permissionsCreated: Int!
21
41
  tagsCreated: Int!
@@ -50,10 +50,11 @@ describe('operation documents', () => {
50
50
  // project-app pictures (eight operation documents).
51
51
  // 135 after project OAuth connections: get/upsert/clear (three operation documents).
52
52
  // 136 after closed-install: AuthSignupPolicyDocument (publicSignupEnabled, bootstrapOpen).
53
+ // 137 after CDM imports by reference: RequestProjectSyncUploadUrlDocument.
53
54
  // Updating this number is the point of the pin, not a nuisance: every change
54
55
  // to it should be a line in a commit message.
55
56
  it('finds every document under src/operations', () => {
56
- expect(files).toHaveLength(136);
57
+ expect(files).toHaveLength(137);
57
58
  });
58
59
 
59
60
  it.each(files.map((f) => [relative(process.cwd(), f), f]))('%s validates', (_label, file) => {