@granular-software/sdk 0.4.6 → 0.4.7

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/README.md CHANGED
@@ -55,16 +55,16 @@ import { Granular, type ManifestContent } from '@granular-software/sdk';
55
55
 
56
56
  const granular = new Granular({ apiKey: process.env.GRANULAR_API_KEY });
57
57
 
58
- // 1. Record user with permissions
59
- const user = await granular.recordUser({
58
+ // 1. Connect to sandbox for one of your app users
59
+ const env = await granular.connect({
60
+ sandbox: 'my-sandbox',
60
61
  userId: 'user_123',
61
62
  permissions: ['agent'],
63
+ name: 'Jane Doe', // optional
64
+ email: 'jane@example.com', // optional
62
65
  });
63
66
 
64
- // 2. Connect to sandbox
65
- const env = await granular.connect({ sandbox: 'my-sandbox', user });
66
-
67
- // 3. Define your domain ontology
67
+ // 2. Define your domain ontology
68
68
  const manifest: ManifestContent = {
69
69
  schemaVersion: 2,
70
70
  name: 'my-app',
@@ -105,7 +105,7 @@ const manifest: ManifestContent = {
105
105
 
106
106
  await env.applyManifest(manifest);
107
107
 
108
- // 4. Record object instances
108
+ // 3. Record object instances
109
109
  await env.recordObject({
110
110
  className: 'customer',
111
111
  id: 'cust_42',
@@ -113,7 +113,7 @@ await env.recordObject({
113
113
  fields: { name: 'Acme Corp', email: 'billing@acme.com', tier: 'enterprise' },
114
114
  });
115
115
 
116
- // 5. Register live effect handlers for effects already declared in the build manifest
116
+ // 4. Register live effect handlers for effects already declared in the build manifest
117
117
  await granular.registerEffects(env.sandboxId, [
118
118
  {
119
119
  name: 'get_billing_summary',
@@ -172,11 +172,11 @@ Effects must be declared ahead of time in the sandbox build manifest with `withE
172
172
  ## Core Flow
173
173
 
174
174
  ```
175
- declare effects in build manifest → connect() → recordObject() → registerEffects() → submitJob()
175
+ declare effects in build manifest → connect({ userId }) → recordObject() → registerEffects() → submitJob()
176
176
  ```
177
177
 
178
- 1. **`recordUser()`** — Register a user and their permission profiles
179
- 2. **`connect()`** — Connect to a sandbox, returning an `Environment`
178
+ 1. **`connect()`** — Connect to a sandbox for a given `userId`, returning an `Environment`
179
+ 2. **`recordUser()`** — Optional explicit user upsert when you want the returned `granularId`
180
180
  3. **`applyManifest()`** — Define your domain ontology (classes, properties, relationships)
181
181
  4. **`recordObject()`** — Create/update instances of your classes with fields and relationships
182
182
  5. **`granular.registerEffects()`** — Register sandbox-scoped live handlers for effects declared in the build manifest
@@ -232,9 +232,15 @@ await env.applyManifest(manifest);
232
232
 
233
233
  ## Recording Object Instances
234
234
 
235
- After defining the ontology, populate it with data:
235
+ After defining the ontology, connect as a user and populate it with data:
236
236
 
237
237
  ```typescript
238
+ const env = await granular.connect({
239
+ sandbox: 'library-app',
240
+ userId: 'user_123',
241
+ permissions: ['agent'],
242
+ });
243
+
238
244
  const tolkien = await env.recordObject({
239
245
  className: 'author',
240
246
  id: 'tolkien', // Real-world ID (unique per class)
package/dist/cli/index.js CHANGED
@@ -8622,17 +8622,12 @@ const granular = new Granular({`);
8622
8622
  lines.push(` ...(process.env.GRANULAR_TOKEN ? { token: process.env.GRANULAR_TOKEN } : { apiKey: process.env.GRANULAR_API_KEY! }),`);
8623
8623
  lines.push(`});`);
8624
8624
  lines.push(`
8625
- // 1. Record the user (creates or updates identity)`);
8626
- lines.push(`const user = await granular.recordUser({`);
8627
- lines.push(` userId: 'user_123', // Your internal ID`);
8628
- lines.push(` email: 'user@example.com',`);
8629
- lines.push(` permissions: ['default'], // Permission profile`);
8630
- lines.push(`});`);
8631
- lines.push(`
8632
- // 2. Connect to the sandbox`);
8625
+ // 1. Connect to the sandbox for one of your app users`);
8633
8626
  lines.push(`const env = await granular.connect({`);
8634
8627
  lines.push(` sandbox: '${sandboxId}',`);
8635
- lines.push(` user,`);
8628
+ lines.push(` userId: 'user_123', // Your app's user ID`);
8629
+ lines.push(` email: 'user@example.com', // optional`);
8630
+ lines.push(` permissions: ['default'], // Permission profile`);
8636
8631
  lines.push(`});`);
8637
8632
  lines.push(`
8638
8633
  console.log('Connected to:', env.environmentId);`);
@@ -8643,6 +8638,12 @@ console.log('Connected to:', env.environmentId);`);
8643
8638
  if (classes.length > 0) {
8644
8639
  lines.push(`
8645
8640
  \`\`\`typescript`);
8641
+ lines.push(`const env = await granular.connect({`);
8642
+ lines.push(` sandbox: '${sandboxId}',`);
8643
+ lines.push(` userId: 'user_123',`);
8644
+ lines.push(` permissions: ['default'],`);
8645
+ lines.push(`});`);
8646
+ lines.push(``);
8646
8647
  for (const cls of classes) {
8647
8648
  const exampleFields = {};
8648
8649
  for (const [k, v] of Object.entries(cls.fields)) {
package/dist/index.d.mts CHANGED
@@ -40,9 +40,13 @@ type GranularAuth = string;
40
40
  * A user/subject object returned from recordUser()
41
41
  */
42
42
  interface User {
43
- /** Internal subject ID */
43
+ /** Internal Granular user identifier */
44
+ granularId: string;
45
+ /** External user identifier from your app */
46
+ userId: string;
47
+ /** @deprecated Use `granularId` instead */
44
48
  subjectId: string;
45
- /** External identity ID (e.g. Auth0 ID) */
49
+ /** @deprecated Use `userId` instead */
46
50
  identityId: string;
47
51
  /** User's display name */
48
52
  name?: string;
@@ -68,6 +72,10 @@ interface RecordUserOptions {
68
72
  * Subject as returned from the API
69
73
  */
70
74
  interface Subject {
75
+ /** Internal Granular user identifier */
76
+ granularId: string;
77
+ /** External user identifier from your app */
78
+ userId: string;
71
79
  subjectId: string;
72
80
  tenantId: string;
73
81
  identityId: string;
@@ -83,8 +91,24 @@ interface Subject {
83
91
  interface ConnectOptions {
84
92
  /** The sandbox name or ID to connect to */
85
93
  sandbox: string;
86
- /** The user to connect as (from recordUser()) */
87
- user: User;
94
+ /**
95
+ * External user identifier from your app. This is the primary input for
96
+ * connecting to a sandbox and the only required user field in the common case.
97
+ */
98
+ userId?: string;
99
+ /**
100
+ * Internal Granular user identifier. Optional fallback when you only know
101
+ * the Granular-side ID for an existing subject.
102
+ */
103
+ granularId?: string;
104
+ /** Optional display name used when upserting the user */
105
+ name?: string;
106
+ /** Optional email used when upserting the user */
107
+ email?: string;
108
+ /** Permission profile IDs or names to ensure before connecting */
109
+ permissions?: string[];
110
+ /** Backwards-compatible user object returned from recordUser() */
111
+ user?: User;
88
112
  /** Optional stable client ID. Defaults to `client_${Date.now()}`. Use a fixed
89
113
  * value for long-lived effect hosts so tool catalogs don't accumulate. */
90
114
  clientId?: string;
@@ -274,6 +298,8 @@ interface EffectHandlerContext {
274
298
  principalId?: string;
275
299
  permissionProfileId?: string;
276
300
  user: {
301
+ granularId?: string;
302
+ userId?: string;
277
303
  subjectId: string;
278
304
  identityId?: string;
279
305
  principalId?: string;
@@ -1017,6 +1043,8 @@ declare class Environment extends Session {
1017
1043
  get sandboxId(): string;
1018
1044
  /** The subject ID */
1019
1045
  get subjectId(): string;
1046
+ /** Internal Granular user identifier for this environment */
1047
+ get granularId(): string;
1020
1048
  /** The permission profile ID */
1021
1049
  get permissionProfileId(): string;
1022
1050
  /** The GraphQL API endpoint URL */
@@ -1330,7 +1358,7 @@ declare class Granular {
1330
1358
  * Records/upserts a user and prepares them for sandbox connections
1331
1359
  *
1332
1360
  * @param options - User options
1333
- * @returns The user object to pass to connect()
1361
+ * @returns The recorded user with both `userId` and `granularId`
1334
1362
  *
1335
1363
  * @example
1336
1364
  * ```typescript
@@ -1342,6 +1370,7 @@ declare class Granular {
1342
1370
  * ```
1343
1371
  */
1344
1372
  recordUser(options: RecordUserOptions): Promise<User>;
1373
+ private resolveConnectUser;
1345
1374
  /**
1346
1375
  * Connect to a sandbox and establish a real-time environment session.
1347
1376
  *
@@ -1354,14 +1383,10 @@ declare class Granular {
1354
1383
  *
1355
1384
  * @example
1356
1385
  * ```typescript
1357
- * const user = await granular.recordUser({
1358
- * userId: 'user_123',
1359
- * permissions: ['agent'],
1360
- * });
1361
- *
1362
1386
  * const environment = await granular.connect({
1363
1387
  * sandbox: 'my-sandbox',
1364
- * user,
1388
+ * userId: 'user_123',
1389
+ * permissions: ['agent'],
1365
1390
  * });
1366
1391
  *
1367
1392
  * await granular.registerEffect('my-sandbox', {
package/dist/index.d.ts CHANGED
@@ -40,9 +40,13 @@ type GranularAuth = string;
40
40
  * A user/subject object returned from recordUser()
41
41
  */
42
42
  interface User {
43
- /** Internal subject ID */
43
+ /** Internal Granular user identifier */
44
+ granularId: string;
45
+ /** External user identifier from your app */
46
+ userId: string;
47
+ /** @deprecated Use `granularId` instead */
44
48
  subjectId: string;
45
- /** External identity ID (e.g. Auth0 ID) */
49
+ /** @deprecated Use `userId` instead */
46
50
  identityId: string;
47
51
  /** User's display name */
48
52
  name?: string;
@@ -68,6 +72,10 @@ interface RecordUserOptions {
68
72
  * Subject as returned from the API
69
73
  */
70
74
  interface Subject {
75
+ /** Internal Granular user identifier */
76
+ granularId: string;
77
+ /** External user identifier from your app */
78
+ userId: string;
71
79
  subjectId: string;
72
80
  tenantId: string;
73
81
  identityId: string;
@@ -83,8 +91,24 @@ interface Subject {
83
91
  interface ConnectOptions {
84
92
  /** The sandbox name or ID to connect to */
85
93
  sandbox: string;
86
- /** The user to connect as (from recordUser()) */
87
- user: User;
94
+ /**
95
+ * External user identifier from your app. This is the primary input for
96
+ * connecting to a sandbox and the only required user field in the common case.
97
+ */
98
+ userId?: string;
99
+ /**
100
+ * Internal Granular user identifier. Optional fallback when you only know
101
+ * the Granular-side ID for an existing subject.
102
+ */
103
+ granularId?: string;
104
+ /** Optional display name used when upserting the user */
105
+ name?: string;
106
+ /** Optional email used when upserting the user */
107
+ email?: string;
108
+ /** Permission profile IDs or names to ensure before connecting */
109
+ permissions?: string[];
110
+ /** Backwards-compatible user object returned from recordUser() */
111
+ user?: User;
88
112
  /** Optional stable client ID. Defaults to `client_${Date.now()}`. Use a fixed
89
113
  * value for long-lived effect hosts so tool catalogs don't accumulate. */
90
114
  clientId?: string;
@@ -274,6 +298,8 @@ interface EffectHandlerContext {
274
298
  principalId?: string;
275
299
  permissionProfileId?: string;
276
300
  user: {
301
+ granularId?: string;
302
+ userId?: string;
277
303
  subjectId: string;
278
304
  identityId?: string;
279
305
  principalId?: string;
@@ -1017,6 +1043,8 @@ declare class Environment extends Session {
1017
1043
  get sandboxId(): string;
1018
1044
  /** The subject ID */
1019
1045
  get subjectId(): string;
1046
+ /** Internal Granular user identifier for this environment */
1047
+ get granularId(): string;
1020
1048
  /** The permission profile ID */
1021
1049
  get permissionProfileId(): string;
1022
1050
  /** The GraphQL API endpoint URL */
@@ -1330,7 +1358,7 @@ declare class Granular {
1330
1358
  * Records/upserts a user and prepares them for sandbox connections
1331
1359
  *
1332
1360
  * @param options - User options
1333
- * @returns The user object to pass to connect()
1361
+ * @returns The recorded user with both `userId` and `granularId`
1334
1362
  *
1335
1363
  * @example
1336
1364
  * ```typescript
@@ -1342,6 +1370,7 @@ declare class Granular {
1342
1370
  * ```
1343
1371
  */
1344
1372
  recordUser(options: RecordUserOptions): Promise<User>;
1373
+ private resolveConnectUser;
1345
1374
  /**
1346
1375
  * Connect to a sandbox and establish a real-time environment session.
1347
1376
  *
@@ -1354,14 +1383,10 @@ declare class Granular {
1354
1383
  *
1355
1384
  * @example
1356
1385
  * ```typescript
1357
- * const user = await granular.recordUser({
1358
- * userId: 'user_123',
1359
- * permissions: ['agent'],
1360
- * });
1361
- *
1362
1386
  * const environment = await granular.connect({
1363
1387
  * sandbox: 'my-sandbox',
1364
- * user,
1388
+ * userId: 'user_123',
1389
+ * permissions: ['agent'],
1365
1390
  * });
1366
1391
  *
1367
1392
  * await granular.registerEffect('my-sandbox', {
package/dist/index.js CHANGED
@@ -4503,6 +4503,8 @@ var Session = class {
4503
4503
  environmentId: "",
4504
4504
  sessionId: "",
4505
4505
  user: {
4506
+ granularId: "",
4507
+ userId: "",
4506
4508
  subjectId: ""
4507
4509
  }
4508
4510
  };
@@ -5197,6 +5199,29 @@ function normalizeHeapSnapshot(raw) {
5197
5199
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
5198
5200
  };
5199
5201
  }
5202
+ function normalizeSubject(subject) {
5203
+ const granularId = subject.granularId || subject.subjectId;
5204
+ const userId = subject.userId || subject.identityId || granularId;
5205
+ return {
5206
+ ...subject,
5207
+ granularId,
5208
+ userId,
5209
+ subjectId: granularId,
5210
+ identityId: subject.identityId || userId
5211
+ };
5212
+ }
5213
+ function normalizeUser(user) {
5214
+ const granularId = user.granularId || user.subjectId;
5215
+ const userId = user.userId || user.identityId || granularId;
5216
+ return {
5217
+ ...user,
5218
+ granularId,
5219
+ userId,
5220
+ subjectId: granularId,
5221
+ identityId: user.identityId || userId,
5222
+ permissions: Array.isArray(user.permissions) ? user.permissions : []
5223
+ };
5224
+ }
5200
5225
  var Environment = class _Environment extends Session {
5201
5226
  envData;
5202
5227
  _apiKey;
@@ -5219,6 +5244,10 @@ var Environment = class _Environment extends Session {
5219
5244
  get subjectId() {
5220
5245
  return this.envData.subjectId;
5221
5246
  }
5247
+ /** Internal Granular user identifier for this environment */
5248
+ get granularId() {
5249
+ return this.envData.subjectId;
5250
+ }
5222
5251
  /** The permission profile ID */
5223
5252
  get permissionProfileId() {
5224
5253
  return this.envData.permissionProfileId;
@@ -6016,7 +6045,7 @@ var Granular = class {
6016
6045
  * Records/upserts a user and prepares them for sandbox connections
6017
6046
  *
6018
6047
  * @param options - User options
6019
- * @returns The user object to pass to connect()
6048
+ * @returns The recorded user with both `userId` and `granularId`
6020
6049
  *
6021
6050
  * @example
6022
6051
  * ```typescript
@@ -6028,21 +6057,61 @@ var Granular = class {
6028
6057
  * ```
6029
6058
  */
6030
6059
  async recordUser(options) {
6031
- const subject = await this.request("/control/subjects", {
6060
+ const subject = normalizeSubject(await this.request("/control/subjects", {
6032
6061
  method: "POST",
6033
6062
  body: JSON.stringify({
6034
6063
  identityId: options.userId,
6035
6064
  name: options.name,
6036
6065
  email: options.email
6037
6066
  })
6038
- });
6039
- return {
6067
+ }));
6068
+ return normalizeUser({
6069
+ granularId: subject.granularId,
6070
+ userId: options.userId,
6040
6071
  subjectId: subject.subjectId,
6041
- identityId: options.userId,
6042
- name: options.name,
6043
- email: options.email,
6072
+ identityId: subject.identityId,
6073
+ name: options.name || subject.name || void 0,
6074
+ email: options.email || subject.email || void 0,
6044
6075
  permissions: options.permissions || []
6045
- };
6076
+ });
6077
+ }
6078
+ async resolveConnectUser(options) {
6079
+ if (options.user) {
6080
+ const user = normalizeUser(options.user);
6081
+ return {
6082
+ granularId: user.granularId,
6083
+ userId: user.userId,
6084
+ name: options.name || user.name,
6085
+ email: options.email || user.email,
6086
+ permissions: options.permissions || user.permissions
6087
+ };
6088
+ }
6089
+ if (options.userId) {
6090
+ const user = await this.recordUser({
6091
+ userId: options.userId,
6092
+ name: options.name,
6093
+ email: options.email,
6094
+ permissions: options.permissions
6095
+ });
6096
+ return {
6097
+ granularId: user.granularId,
6098
+ userId: user.userId,
6099
+ name: user.name,
6100
+ email: user.email,
6101
+ permissions: options.permissions || user.permissions
6102
+ };
6103
+ }
6104
+ if (options.granularId) {
6105
+ const subject = await this.subjects.get(options.granularId);
6106
+ return {
6107
+ granularId: subject.granularId,
6108
+ userId: subject.userId,
6109
+ name: options.name || subject.name || void 0,
6110
+ email: options.email || subject.email || void 0,
6111
+ permissions: options.permissions || []
6112
+ };
6113
+ }
6114
+ throw new Error("connect() requires either userId, granularId, or a user object returned by recordUser().");
6046
6115
  }
6047
6116
  /**
6048
6117
  * Connect to a sandbox and establish a real-time environment session.
@@ -6056,14 +6125,10 @@ var Granular = class {
6056
6125
  *
6057
6126
  * @example
6058
6127
  * ```typescript
6059
- * const user = await granular.recordUser({
6060
- * userId: 'user_123',
6061
- * permissions: ['agent'],
6062
- * });
6063
- *
6064
6128
  * const environment = await granular.connect({
6065
6129
  * sandbox: 'my-sandbox',
6066
- * user,
6130
+ * userId: 'user_123',
6131
+ * permissions: ['agent'],
6067
6132
  * });
6068
6133
  *
6069
6134
  * await granular.registerEffect('my-sandbox', {
@@ -6084,13 +6149,14 @@ var Granular = class {
6084
6149
  */
6085
6150
  async connect(options) {
6086
6151
  const clientId = options.clientId || `client_${Date.now()}`;
6152
+ const user = await this.resolveConnectUser(options);
6087
6153
  const sandbox = await this.findOrCreateSandbox(options.sandbox);
6088
- for (const profileName of options.user.permissions) {
6154
+ for (const profileName of user.permissions) {
6089
6155
  const profileId = await this.ensurePermissionProfile(sandbox.sandboxId, profileName);
6090
- await this.ensureAssignment(options.user.subjectId, sandbox.sandboxId, profileId);
6156
+ await this.ensureAssignment(user.granularId, sandbox.sandboxId, profileId);
6091
6157
  }
6092
6158
  const envData = await this.environments.create(sandbox.sandboxId, {
6093
- subjectId: options.user.subjectId,
6159
+ subjectId: user.granularId,
6094
6160
  permissionProfileId: null
6095
6161
  });
6096
6162
  await this.activateEnvironment(envData.environmentId);
@@ -6523,7 +6589,7 @@ var Granular = class {
6523
6589
  get subjects() {
6524
6590
  return {
6525
6591
  get: async (subjectId) => {
6526
- return this.request(`/control/subjects/${subjectId}`);
6592
+ return normalizeSubject(await this.request(`/control/subjects/${subjectId}`));
6527
6593
  },
6528
6594
  listAssignments: async (subjectId) => {
6529
6595
  return this.request(`/control/subjects/${subjectId}/assignments`);
@@ -6536,17 +6602,17 @@ var Granular = class {
6536
6602
  get users() {
6537
6603
  return {
6538
6604
  create: async (data) => {
6539
- return this.request("/control/subjects", {
6605
+ return normalizeSubject(await this.request("/control/subjects", {
6540
6606
  method: "POST",
6541
6607
  body: JSON.stringify({
6542
6608
  identityId: data.id,
6543
6609
  name: data.name,
6544
6610
  email: data.email
6545
6611
  })
6546
- });
6612
+ }));
6547
6613
  },
6548
6614
  get: async (id) => {
6549
- return this.request(`/control/subjects/${id}`);
6615
+ return normalizeSubject(await this.request(`/control/subjects/${id}`));
6550
6616
  }
6551
6617
  };
6552
6618
  }