@lanes-sh/link 0.6.4 → 0.6.5

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": "@lanes-sh/link",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "A self-hostable MCP gateway for all your connections, memory, tasks, files, and secrets",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://lanes.sh/link",
@@ -157,6 +157,7 @@ export async function deploy(flags: DeployFlags): Promise<void> {
157
157
  target,
158
158
  rotatable,
159
159
  readable,
160
+ profiles: serving,
160
161
  });
161
162
 
162
163
  // Where the running instance will read its config. The bucket the target
@@ -92,6 +92,20 @@ export interface ProvisionInput {
92
92
  * what a target with no profile to walk still needs.
93
93
  */
94
94
  readonly readable?: readonly string[];
95
+ /**
96
+ * The profiles this revision serves, so the bucket conditions can name each
97
+ * one's provider manifests.
98
+ *
99
+ * Needed because Cloud Storage IAM conditions cannot express "any profile
100
+ * segment": their CEL is a restricted subset with no `matches`, so the only
101
+ * way to carve out `data/<profile>/providers.d/` is to enumerate the profiles
102
+ * and write one `startsWith` each. `deploy.ts` already resolved the list.
103
+ *
104
+ * Absent leaves the carve-out off entirely rather than guessing, which is the
105
+ * safe direction: the revision keeps write on its own data and the manifests
106
+ * inside it, exactly as it did before the carve-out existed.
107
+ */
108
+ readonly profiles?: readonly string[];
95
109
  }
96
110
 
97
111
  export interface SurveyInput {
@@ -1,6 +1,7 @@
1
1
  import { VAULT_DOCUMENT_REF, type SecretRef } from '#secrets';
2
2
  import type { DeployStep, ProvisionInput } from '../driver.ts';
3
3
  import { encodeRef } from '../adapters/gcp-secret-manager.ts';
4
+ import { layout } from '#profile';
4
5
  import { requireProject } from './gcloud.ts';
5
6
 
6
7
  /**
@@ -296,22 +297,34 @@ export function provisionSteps(input: ProvisionInput): Promise<DeployStep[]> {
296
297
  `resource.name == "projects/_/buckets/${bucket}/objects/${path}"`;
297
298
 
298
299
  // A provider manifest is configuration that happens to live inside the
299
- // profile's directory (ADR-030), so `data/` alone no longer separates
300
- // what the revision owns from what declares what it is. Anchored to the
301
- // profile segment rather than matched loosely: `contains("/providers.d/")`
302
- // would also catch a blob whose own key happened to spell it.
300
+ // profile's directory (ADR-030), so `data/` alone no longer separates what
301
+ // the revision owns from what declares what it is.
303
302
  //
304
- // The dot is a character class, not `\.`, and that is the fix rather than
305
- // a style: this string is a *CEL string literal* holding a regex, so it is
306
- // unescaped once by CEL before the regex engine ever sees it. `\.` is not
307
- // a CEL escape sequence, so the whole expression failed to compile
308
- // `token recognition error at: '"^projects/_/buckets/...providers\.'`
309
- // and both bindings below carry `tolerateFailure`, so a deploy printed two
310
- // warnings and carried on with the scoping silently not applied. `[.]` is
311
- // the same regex and survives a layer of string unescaping unchanged,
312
- // which is what keeps the next person from reintroducing it.
303
+ // **One `startsWith` per profile, because Cloud Storage IAM conditions
304
+ // cannot express anything else.** Their CEL is a restricted subset
305
+ // `resource.type`, `resource.name` with `startsWith`/`endsWith`/`==`, and
306
+ // the date functions and it has no `matches`. This was a regex, and it
307
+ // was refused twice over: first because it spelled the dot `\.`, which is
308
+ // not a CEL escape, so the string literal would not parse; then, with that
309
+ // fixed, because `matches` is `undeclared` in this dialect.
310
+ //
311
+ // Both bindings carry `tolerateFailure`, so each attempt printed a warning
312
+ // and left whatever conditions the bucket already had. On a real
313
+ // deployment that was `expression=true` on the read binding — every object
314
+ // in the bucket, the exact opposite of the narrowing the step title claims,
315
+ // and the state ADR-007 says must not exist.
316
+ //
317
+ // Enumerating the served profiles is expressible in the subset that does
318
+ // exist, and it makes `grants.test.ts` honest as a side effect: that file
319
+ // evaluates these as JavaScript, where `startsWith` means what it means
320
+ // here and `matches` quietly did not.
321
+ const manifestPrefixes = (input.profiles ?? []).map((profile) =>
322
+ objectsUnder(`${layout.providers(profile)}/`),
323
+ );
324
+ // No profiles leaves the carve-out off rather than guessing at one: the
325
+ // revision keeps write on its own data, as it did before this existed.
313
326
  const providerManifests =
314
- `resource.name.matches("^projects/_/buckets/${bucket}/objects/data/[^/]+/providers[.]d/")`;
327
+ manifestPrefixes.length > 0 ? `(${manifestPrefixes.join(' || ')})` : null;
315
328
 
316
329
  steps.push({
317
330
  title: 'let the revision write its own data, but not the manifests in it',
@@ -327,7 +340,7 @@ export function provisionSteps(input: ProvisionInput): Promise<DeployStep[]> {
327
340
  '--role',
328
341
  'roles/storage.objectAdmin',
329
342
  '--condition',
330
- `title=owns-its-data,expression=${objectsUnder('data/')} && !${providerManifests}`,
343
+ `title=owns-its-data,expression=${objectsUnder('data/')}${providerManifests ? ` && !${providerManifests}` : ''}`,
331
344
  ],
332
345
  tolerateFailure: true,
333
346
  });
@@ -348,7 +361,7 @@ export function provisionSteps(input: ProvisionInput): Promise<DeployStep[]> {
348
361
  // The config the revision reads is the workspace file, the profiles
349
362
  // beside it, and each profile's own manifests, so name exactly those.
350
363
  '--condition',
351
- `title=reads-its-config,expression=${objectsUnder('profiles/')} || ${objectIs('lanes-link.yaml')} || ${providerManifests}`,
364
+ `title=reads-its-config,expression=${objectsUnder('profiles/')} || ${objectIs('lanes-link.yaml')}${providerManifests ? ` || ${providerManifests}` : ''}`,
352
365
  ],
353
366
  tolerateFailure: true,
354
367
  });