node-type-registry 0.38.0 → 0.40.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.
@@ -808,8 +808,10 @@ export interface BlueprintBucketSeed {
808
808
  /** CORS allowed origins for this bucket. */
809
809
  allowed_origins?: string[];
810
810
  }
811
- /** Storage configuration for an entity type. Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions. */
811
+ /** Storage configuration with optional scope. When used at the top level of a blueprint, the scope field controls whether storage is app-level ("app", default) or org-level ("org"). Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions. */
812
812
  export interface BlueprintStorageConfig {
813
+ /** Storage scope. "app" (default) creates app-level storage (no owner_id). "org" creates per-org/user storage (owner_id = org entity id, buckets seeded per-entity via AFTER INSERT trigger). Only "app" and "org" are allowed — child entity types get storage via entity_types[].storage. */
814
+ scope?: 'app' | 'org';
813
815
  /** Discriminator for multi-module storage. Defaults to "default" (omitted from table names). Non-default keys appear as an infix: {prefix}_{storage_key}_buckets. Max 16 chars, lowercase snake_case. */
814
816
  storage_key?: string;
815
817
  /** Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning. */
@@ -883,10 +885,10 @@ export interface BlueprintEntityTableProvision {
883
885
  /** RLS policies for the entity table. When present, these policies fully replace the five default entity-table policies (is_visible becomes a no-op). */
884
886
  policies?: BlueprintPolicy[];
885
887
  }
886
- /** An entity type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision. */
888
+ /** An entity type entry for Phase 0 of construct_blueprint(). When name is provided, provisions a new entity type with its own entity table, membership modules, and security policies via entity_type_provision. When name is omitted and only prefix is given, extends an existing entity type (e.g., the built-in "org") with additional capabilities like storage — without creating a new entity type. */
887
889
  export interface BlueprintEntityType {
888
- /** Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database. */
889
- name: string;
890
+ /** Entity type name (e.g., "data_room", "channel", "department"). Required when creating a new entity type. Omit when extending an existing entity type (e.g., prefix: "org") — the entry will add storage/config to the existing type without creating a new one. */
891
+ name?: string;
890
892
  /** Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming. */
891
893
  prefix: string;
892
894
  /** Human-readable description of this entity type. */
@@ -1177,7 +1179,7 @@ export interface BlueprintDefinition {
1177
1179
  unique_constraints?: BlueprintUniqueConstraint[];
1178
1180
  /** Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security. */
1179
1181
  entity_types?: BlueprintEntityType[];
1180
- /** App-level storage configuration (array-only). Creates storage_module(s) (membership_type = NULL), seeds initial buckets, and overrides module-level settings. Each entry creates a separate storage module. For entity-scoped storage, use entity_types[].storage instead. */
1182
+ /** Top-level storage configuration array. Each entry has an optional scope ("app" or "org"). App-scoped (default) creates storage_module with membership_type = NULL. Org-scoped creates per-org/user storage with owner_id and AFTER INSERT bucket seeding. When infra is installed, a private "functions" bucket is auto-injected into org-scoped entries. For child entity type storage, use entity_types[].storage instead. */
1181
1183
  storage?: BlueprintStorageConfig[];
1182
1184
  /** Achievement definitions. Each entry creates a level with requirements and optional rewards in the events_module. Requires events_module to be provisioned (e.g., via entity_types[].has_levels = true or modules includes events_module). */
1183
1185
  achievements?: BlueprintAchievement[];
@@ -515,6 +515,10 @@ function buildBlueprintBucketSeed() {
515
515
  */
516
516
  function buildBlueprintStorageConfig() {
517
517
  return addJSDoc(exportInterface('BlueprintStorageConfig', [
518
+ addJSDoc(optionalProp('scope', t.tsUnionType([
519
+ t.tsLiteralType(t.stringLiteral('app')),
520
+ t.tsLiteralType(t.stringLiteral('org'))
521
+ ])), 'Storage scope. "app" (default) creates app-level storage (no owner_id). "org" creates per-org/user storage (owner_id = org entity id, buckets seeded per-entity via AFTER INSERT trigger). Only "app" and "org" are allowed — child entity types get storage via entity_types[].storage.'),
518
522
  addJSDoc(optionalProp('storage_key', t.tsStringKeyword()), 'Discriminator for multi-module storage. Defaults to "default" (omitted from table names). Non-default keys appear as an infix: {prefix}_{storage_key}_buckets. Max 16 chars, lowercase snake_case.'),
519
523
  addJSDoc(optionalProp('buckets', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintBucketSeed')))), 'Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning.'),
520
524
  addJSDoc(optionalProp('upload_url_expiry_seconds', t.tsNumberKeyword()), 'Override for presigned upload URL expiry time in seconds.'),
@@ -527,7 +531,7 @@ function buildBlueprintStorageConfig() {
527
531
  optionalProp('files', t.tsTypeReference(t.identifier('BlueprintEntityTableProvision'))),
528
532
  optionalProp('buckets', t.tsTypeReference(t.identifier('BlueprintEntityTableProvision')))
529
533
  ])), 'Per-table overrides for storage tables. Each key targets a specific storage table (files, buckets) and uses the same shape as table_provision: { nodes, fields, grants, use_rls, policies }. Fanned out to secure_table_provision targeting the corresponding table. When a key includes policies[], those REPLACE the default storage policies for that table; tables without a key still get defaults.')
530
- ]), 'Storage configuration for an entity type. Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions.');
534
+ ]), 'Storage configuration with optional scope. When used at the top level of a blueprint, the scope field controls whether storage is app-level (\"app\", default) or org-level (\"org\"). Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions.');
531
535
  }
532
536
  // ---------------------------------------------------------------------------
533
537
  // Achievement types
@@ -574,7 +578,7 @@ function buildBlueprintEntityTableProvision() {
574
578
  }
575
579
  function buildBlueprintEntityType() {
576
580
  return addJSDoc(exportInterface('BlueprintEntityType', [
577
- addJSDoc(requiredProp('name', t.tsStringKeyword()), 'Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database.'),
581
+ addJSDoc(optionalProp('name', t.tsStringKeyword()), 'Entity type name (e.g., "data_room", "channel", "department"). Required when creating a new entity type. Omit when extending an existing entity type (e.g., prefix: "org") — the entry will add storage/config to the existing type without creating a new one.'),
578
582
  addJSDoc(requiredProp('prefix', t.tsStringKeyword()), 'Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming.'),
579
583
  addJSDoc(optionalProp('description', t.tsStringKeyword()), 'Human-readable description of this entity type.'),
580
584
  addJSDoc(optionalProp('parent_entity', t.tsStringKeyword()), 'Parent entity type name. Defaults to "org".'),
@@ -588,7 +592,7 @@ function buildBlueprintEntityType() {
588
592
  addJSDoc(optionalProp('skip_entity_policies', t.tsBooleanKeyword()), 'Escape hatch: when true AND table_provision is NULL, zero policies are provisioned on the entity table. Defaults to false.'),
589
593
  addJSDoc(optionalProp('table_provision', t.tsTypeReference(t.identifier('BlueprintEntityTableProvision'))), 'Override for the entity table. Shape mirrors BlueprintTable / secure_table_provision vocabulary. When supplied, its policies[] replaces the five default entity-table policies; is_visible becomes a no-op. When NULL (default), the five default policies are applied (gated by is_visible).'),
590
594
  addJSDoc(optionalProp('storage', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintStorageConfig')))), 'Storage module configuration array. Each entry provisions a separate storage module with its own tables, RLS, and settings. When non-empty, has_storage is derived as true. Each entry may specify a storage_key for multi-module support (defaults to "default").')
591
- ]), 'An entity type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision.');
595
+ ]), 'An entity type entry for Phase 0 of construct_blueprint(). When name is provided, provisions a new entity type with its own entity table, membership modules, and security policies via entity_type_provision. When name is omitted and only prefix is given, extends an existing entity type (e.g., the built-in "org") with additional capabilities like storage — without creating a new entity type.');
592
596
  }
593
597
  function buildBlueprintTable() {
594
598
  return addJSDoc(exportInterface('BlueprintTable', [
@@ -615,7 +619,7 @@ function buildBlueprintDefinition() {
615
619
  addJSDoc(optionalProp('full_text_searches', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFullTextSearch')))), 'Full-text search configurations.'),
616
620
  addJSDoc(optionalProp('unique_constraints', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintUniqueConstraint')))), 'Unique constraints on table columns.'),
617
621
  addJSDoc(optionalProp('entity_types', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintEntityType')))), 'Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security.'),
618
- addJSDoc(optionalProp('storage', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintStorageConfig')))), 'App-level storage configuration array. Each entry creates a storage_module (membership_type = NULL) with its own tables and settings. For entity-scoped storage, use entity_types[].storage instead.'),
622
+ addJSDoc(optionalProp('storage', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintStorageConfig')))), 'Top-level storage configuration array. Each entry has an optional scope ("app" or "org"). App-scoped (default) creates storage_module with membership_type = NULL. Org-scoped creates per-org/user storage with owner_id and AFTER INSERT bucket seeding. When infra is installed, a private "functions" bucket is auto-injected into org-scoped entries. For child entity type storage, use entity_types[].storage instead.'),
619
623
  addJSDoc(optionalProp('achievements', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintAchievement')))), 'Achievement definitions. Each entry creates a level with requirements and optional rewards in the events_module. Requires events_module to be provisioned (e.g., via entity_types[].has_levels = true or modules includes events_module).')
620
624
  ]), 'The complete blueprint definition -- the JSONB shape accepted by construct_blueprint().');
621
625
  }
@@ -808,8 +808,10 @@ export interface BlueprintBucketSeed {
808
808
  /** CORS allowed origins for this bucket. */
809
809
  allowed_origins?: string[];
810
810
  }
811
- /** Storage configuration for an entity type. Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions. */
811
+ /** Storage configuration with optional scope. When used at the top level of a blueprint, the scope field controls whether storage is app-level ("app", default) or org-level ("org"). Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions. */
812
812
  export interface BlueprintStorageConfig {
813
+ /** Storage scope. "app" (default) creates app-level storage (no owner_id). "org" creates per-org/user storage (owner_id = org entity id, buckets seeded per-entity via AFTER INSERT trigger). Only "app" and "org" are allowed — child entity types get storage via entity_types[].storage. */
814
+ scope?: 'app' | 'org';
813
815
  /** Discriminator for multi-module storage. Defaults to "default" (omitted from table names). Non-default keys appear as an infix: {prefix}_{storage_key}_buckets. Max 16 chars, lowercase snake_case. */
814
816
  storage_key?: string;
815
817
  /** Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning. */
@@ -883,10 +885,10 @@ export interface BlueprintEntityTableProvision {
883
885
  /** RLS policies for the entity table. When present, these policies fully replace the five default entity-table policies (is_visible becomes a no-op). */
884
886
  policies?: BlueprintPolicy[];
885
887
  }
886
- /** An entity type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision. */
888
+ /** An entity type entry for Phase 0 of construct_blueprint(). When name is provided, provisions a new entity type with its own entity table, membership modules, and security policies via entity_type_provision. When name is omitted and only prefix is given, extends an existing entity type (e.g., the built-in "org") with additional capabilities like storage — without creating a new entity type. */
887
889
  export interface BlueprintEntityType {
888
- /** Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database. */
889
- name: string;
890
+ /** Entity type name (e.g., "data_room", "channel", "department"). Required when creating a new entity type. Omit when extending an existing entity type (e.g., prefix: "org") — the entry will add storage/config to the existing type without creating a new one. */
891
+ name?: string;
890
892
  /** Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming. */
891
893
  prefix: string;
892
894
  /** Human-readable description of this entity type. */
@@ -1177,7 +1179,7 @@ export interface BlueprintDefinition {
1177
1179
  unique_constraints?: BlueprintUniqueConstraint[];
1178
1180
  /** Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security. */
1179
1181
  entity_types?: BlueprintEntityType[];
1180
- /** App-level storage configuration (array-only). Creates storage_module(s) (membership_type = NULL), seeds initial buckets, and overrides module-level settings. Each entry creates a separate storage module. For entity-scoped storage, use entity_types[].storage instead. */
1182
+ /** Top-level storage configuration array. Each entry has an optional scope ("app" or "org"). App-scoped (default) creates storage_module with membership_type = NULL. Org-scoped creates per-org/user storage with owner_id and AFTER INSERT bucket seeding. When infra is installed, a private "functions" bucket is auto-injected into org-scoped entries. For child entity type storage, use entity_types[].storage instead. */
1181
1183
  storage?: BlueprintStorageConfig[];
1182
1184
  /** Achievement definitions. Each entry creates a level with requirements and optional rewards in the events_module. Requires events_module to be provisioned (e.g., via entity_types[].has_levels = true or modules includes events_module). */
1183
1185
  achievements?: BlueprintAchievement[];
@@ -480,6 +480,10 @@ function buildBlueprintBucketSeed() {
480
480
  */
481
481
  function buildBlueprintStorageConfig() {
482
482
  return addJSDoc(exportInterface('BlueprintStorageConfig', [
483
+ addJSDoc(optionalProp('scope', t.tsUnionType([
484
+ t.tsLiteralType(t.stringLiteral('app')),
485
+ t.tsLiteralType(t.stringLiteral('org'))
486
+ ])), 'Storage scope. "app" (default) creates app-level storage (no owner_id). "org" creates per-org/user storage (owner_id = org entity id, buckets seeded per-entity via AFTER INSERT trigger). Only "app" and "org" are allowed — child entity types get storage via entity_types[].storage.'),
483
487
  addJSDoc(optionalProp('storage_key', t.tsStringKeyword()), 'Discriminator for multi-module storage. Defaults to "default" (omitted from table names). Non-default keys appear as an infix: {prefix}_{storage_key}_buckets. Max 16 chars, lowercase snake_case.'),
484
488
  addJSDoc(optionalProp('buckets', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintBucketSeed')))), 'Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning.'),
485
489
  addJSDoc(optionalProp('upload_url_expiry_seconds', t.tsNumberKeyword()), 'Override for presigned upload URL expiry time in seconds.'),
@@ -492,7 +496,7 @@ function buildBlueprintStorageConfig() {
492
496
  optionalProp('files', t.tsTypeReference(t.identifier('BlueprintEntityTableProvision'))),
493
497
  optionalProp('buckets', t.tsTypeReference(t.identifier('BlueprintEntityTableProvision')))
494
498
  ])), 'Per-table overrides for storage tables. Each key targets a specific storage table (files, buckets) and uses the same shape as table_provision: { nodes, fields, grants, use_rls, policies }. Fanned out to secure_table_provision targeting the corresponding table. When a key includes policies[], those REPLACE the default storage policies for that table; tables without a key still get defaults.')
495
- ]), 'Storage configuration for an entity type. Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions.');
499
+ ]), 'Storage configuration with optional scope. When used at the top level of a blueprint, the scope field controls whether storage is app-level (\"app\", default) or org-level (\"org\"). Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions.');
496
500
  }
497
501
  // ---------------------------------------------------------------------------
498
502
  // Achievement types
@@ -539,7 +543,7 @@ function buildBlueprintEntityTableProvision() {
539
543
  }
540
544
  function buildBlueprintEntityType() {
541
545
  return addJSDoc(exportInterface('BlueprintEntityType', [
542
- addJSDoc(requiredProp('name', t.tsStringKeyword()), 'Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database.'),
546
+ addJSDoc(optionalProp('name', t.tsStringKeyword()), 'Entity type name (e.g., "data_room", "channel", "department"). Required when creating a new entity type. Omit when extending an existing entity type (e.g., prefix: "org") — the entry will add storage/config to the existing type without creating a new one.'),
543
547
  addJSDoc(requiredProp('prefix', t.tsStringKeyword()), 'Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming.'),
544
548
  addJSDoc(optionalProp('description', t.tsStringKeyword()), 'Human-readable description of this entity type.'),
545
549
  addJSDoc(optionalProp('parent_entity', t.tsStringKeyword()), 'Parent entity type name. Defaults to "org".'),
@@ -553,7 +557,7 @@ function buildBlueprintEntityType() {
553
557
  addJSDoc(optionalProp('skip_entity_policies', t.tsBooleanKeyword()), 'Escape hatch: when true AND table_provision is NULL, zero policies are provisioned on the entity table. Defaults to false.'),
554
558
  addJSDoc(optionalProp('table_provision', t.tsTypeReference(t.identifier('BlueprintEntityTableProvision'))), 'Override for the entity table. Shape mirrors BlueprintTable / secure_table_provision vocabulary. When supplied, its policies[] replaces the five default entity-table policies; is_visible becomes a no-op. When NULL (default), the five default policies are applied (gated by is_visible).'),
555
559
  addJSDoc(optionalProp('storage', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintStorageConfig')))), 'Storage module configuration array. Each entry provisions a separate storage module with its own tables, RLS, and settings. When non-empty, has_storage is derived as true. Each entry may specify a storage_key for multi-module support (defaults to "default").')
556
- ]), 'An entity type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision.');
560
+ ]), 'An entity type entry for Phase 0 of construct_blueprint(). When name is provided, provisions a new entity type with its own entity table, membership modules, and security policies via entity_type_provision. When name is omitted and only prefix is given, extends an existing entity type (e.g., the built-in "org") with additional capabilities like storage — without creating a new entity type.');
557
561
  }
558
562
  function buildBlueprintTable() {
559
563
  return addJSDoc(exportInterface('BlueprintTable', [
@@ -580,7 +584,7 @@ function buildBlueprintDefinition() {
580
584
  addJSDoc(optionalProp('full_text_searches', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFullTextSearch')))), 'Full-text search configurations.'),
581
585
  addJSDoc(optionalProp('unique_constraints', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintUniqueConstraint')))), 'Unique constraints on table columns.'),
582
586
  addJSDoc(optionalProp('entity_types', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintEntityType')))), 'Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security.'),
583
- addJSDoc(optionalProp('storage', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintStorageConfig')))), 'App-level storage configuration array. Each entry creates a storage_module (membership_type = NULL) with its own tables and settings. For entity-scoped storage, use entity_types[].storage instead.'),
587
+ addJSDoc(optionalProp('storage', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintStorageConfig')))), 'Top-level storage configuration array. Each entry has an optional scope ("app" or "org"). App-scoped (default) creates storage_module with membership_type = NULL. Org-scoped creates per-org/user storage with owner_id and AFTER INSERT bucket seeding. When infra is installed, a private "functions" bucket is auto-injected into org-scoped entries. For child entity type storage, use entity_types[].storage instead.'),
584
588
  addJSDoc(optionalProp('achievements', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintAchievement')))), 'Achievement definitions. Each entry creates a level with requirements and optional rewards in the events_module. Requires events_module to be provisioned (e.g., via entity_types[].has_levels = true or modules includes events_module).')
585
589
  ]), 'The complete blueprint definition -- the JSONB shape accepted by construct_blueprint().');
586
590
  }
@@ -41,6 +41,16 @@ export const EventReferral = {
41
41
  format: 'column-ref',
42
42
  description: 'Column containing the entity ID (org/group) for entity-scoped referral events. Omit for user-only events.'
43
43
  },
44
+ max_depth: {
45
+ type: 'integer',
46
+ description: 'Maximum depth to walk up the invite chain. ' +
47
+ 'Default 1 (direct inviter only). Set 2–10 to enable ' +
48
+ 'multi-level referral rewards. App-level only — must not ' +
49
+ 'be combined with entity_field.',
50
+ default: 1,
51
+ minimum: 1,
52
+ maximum: 10,
53
+ },
44
54
  auto_register_type: {
45
55
  type: 'boolean',
46
56
  description: 'Automatically register the event_name in event_types during provisioning',
package/event/referral.js CHANGED
@@ -44,6 +44,16 @@ exports.EventReferral = {
44
44
  format: 'column-ref',
45
45
  description: 'Column containing the entity ID (org/group) for entity-scoped referral events. Omit for user-only events.'
46
46
  },
47
+ max_depth: {
48
+ type: 'integer',
49
+ description: 'Maximum depth to walk up the invite chain. ' +
50
+ 'Default 1 (direct inviter only). Set 2–10 to enable ' +
51
+ 'multi-level referral rewards. App-level only — must not ' +
52
+ 'be combined with entity_field.',
53
+ default: 1,
54
+ minimum: 1,
55
+ maximum: 10,
56
+ },
47
57
  auto_register_type: {
48
58
  type: 'boolean',
49
59
  description: 'Automatically register the event_name in event_types during provisioning',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-type-registry",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Node type definitions for the Constructive blueprint system. Single source of truth for all Authz*, Data*, Relation*, and View* node types.",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "main": "index.js",
@@ -47,5 +47,5 @@
47
47
  "registry",
48
48
  "graphile"
49
49
  ],
50
- "gitHead": "1aaafe14a8ba4eeeaab099f5fdc69865ce4e2a2e"
50
+ "gitHead": "35e09818297d7ef14a0aa1ed723d7dd0de7cb83a"
51
51
  }