@kehto/services 0.17.0 → 0.18.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/dist/index.js CHANGED
@@ -3171,14 +3171,17 @@ function hasOwn(value, key) {
3171
3171
  }
3172
3172
  function validateIntentRequest(value) {
3173
3173
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
3174
- return { error: "invalid convention" };
3174
+ return { error: "invalid request" };
3175
3175
  }
3176
3176
  const request = value;
3177
- if (typeof request.archetype !== "string" || request.archetype.length === 0 || typeof request.action !== "string" || request.action.length === 0 || typeof request.convention !== "string" || request.convention.length === 0) {
3178
- return { error: "invalid convention" };
3177
+ if (typeof request.archetype !== "string" || request.archetype.length === 0) {
3178
+ return { error: "invalid request" };
3179
+ }
3180
+ const action = request.action === void 0 ? "open" : request.action;
3181
+ if (typeof action !== "string" || action.length === 0) {
3182
+ return { error: "invalid request" };
3179
3183
  }
3180
- const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(request.convention);
3181
- if (!match || match[1] !== request.archetype || match[2] !== request.action) {
3184
+ if (request.convention !== void 0 && (typeof request.convention !== "string" || !/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(request.convention))) {
3182
3185
  return { error: "invalid convention" };
3183
3186
  }
3184
3187
  const allowedKeys = ["archetype", "action", "convention", "payload", "handler", "behavior"];
@@ -3194,19 +3197,20 @@ function validateIntentRequest(value) {
3194
3197
  return { error: "invoke rejected" };
3195
3198
  }
3196
3199
  const record = request.behavior;
3197
- if (Object.keys(record).some((key) => key !== "focus" && key !== "reuse") || hasOwn(record, "focus") && typeof record.focus !== "boolean" || hasOwn(record, "reuse") && typeof record.reuse !== "boolean") {
3200
+ if (Object.keys(record).some((key) => key !== "focus" && key !== "newWindow" && key !== "reuse") || hasOwn(record, "focus") && typeof record.focus !== "boolean" || hasOwn(record, "newWindow") && typeof record.newWindow !== "boolean" || hasOwn(record, "reuse") && typeof record.reuse !== "boolean") {
3198
3201
  return { error: "invoke rejected" };
3199
3202
  }
3200
3203
  behavior = {
3201
3204
  ...record.focus === void 0 ? {} : { focus: record.focus },
3205
+ ...record.newWindow === void 0 ? {} : { newWindow: record.newWindow },
3202
3206
  ...record.reuse === void 0 ? {} : { reuse: record.reuse }
3203
3207
  };
3204
3208
  }
3205
3209
  return {
3206
3210
  request: {
3207
3211
  archetype: request.archetype,
3208
- action: request.action,
3209
- convention: request.convention,
3212
+ action,
3213
+ ...typeof request.convention === "string" ? { convention: request.convention } : {},
3210
3214
  ...hasOwn(request, "payload") ? { payload: request.payload } : {},
3211
3215
  ...typeof request.handler === "string" ? { handler: request.handler } : {},
3212
3216
  ...behavior === void 0 ? {} : { behavior }
@@ -3223,11 +3227,20 @@ function settleResolverCall(call, send, resultType, id, onValue) {
3223
3227
  }
3224
3228
  pending.then((value) => send(onValue(value))).catch((error) => send({ type: resultType, id, error: toErrorMessage4(error) }));
3225
3229
  }
3226
- function rejectInvoke(send, id) {
3230
+ function rejectedResult(request, error) {
3231
+ return {
3232
+ ok: false,
3233
+ archetype: request.archetype,
3234
+ action: request.action ?? "open",
3235
+ handled: false,
3236
+ error
3237
+ };
3238
+ }
3239
+ function rejectInvoke(send, id, request, error = "invoke rejected") {
3227
3240
  send({
3228
3241
  type: "intent.invoke.result",
3229
3242
  id,
3230
- result: { ok: false, error: "invoke rejected" }
3243
+ result: rejectedResult(request, error)
3231
3244
  });
3232
3245
  }
3233
3246
  function createIntentService(options) {
@@ -3242,46 +3255,40 @@ function createIntentService(options) {
3242
3255
  const id = m.id ?? "";
3243
3256
  const validation = validateIntentRequest(m.request);
3244
3257
  if ("error" in validation) {
3245
- send({
3246
- type: "intent.invoke.result",
3258
+ const raw = typeof m.request === "object" && m.request !== null ? m.request : {};
3259
+ rejectInvoke(
3260
+ send,
3247
3261
  id,
3248
- result: { ok: false, error: validation.error }
3249
- });
3262
+ {
3263
+ archetype: typeof raw.archetype === "string" ? raw.archetype : "",
3264
+ action: typeof raw.action === "string" ? raw.action : "open"
3265
+ },
3266
+ validation.error
3267
+ );
3250
3268
  return;
3251
3269
  }
3252
3270
  const sender = runtimeContext?.resolveDTag(windowId);
3253
3271
  if (!sender) {
3254
- rejectInvoke(send, id);
3272
+ rejectInvoke(send, id, validation.request);
3255
3273
  return;
3256
3274
  }
3257
3275
  let pending;
3258
3276
  try {
3259
3277
  pending = Promise.resolve(resolver.invoke(validation.request, { sender }));
3260
3278
  } catch {
3261
- rejectInvoke(send, id);
3279
+ rejectInvoke(send, id, validation.request);
3262
3280
  return;
3263
3281
  }
3264
3282
  void pending.then(
3265
- (outcome) => {
3266
- if (outcome.result.ok && (!("retained" in outcome) || typeof outcome.retained.start !== "function")) {
3267
- rejectInvoke(send, id);
3268
- return;
3269
- }
3283
+ (result2) => {
3270
3284
  send({
3271
3285
  type: "intent.invoke.result",
3272
3286
  id,
3273
- result: outcome.result
3287
+ result: result2
3274
3288
  });
3275
- if (!outcome.result.ok || !("retained" in outcome)) return;
3276
- try {
3277
- const started = outcome.retained.start();
3278
- void Promise.resolve(started).catch(() => {
3279
- });
3280
- } catch {
3281
- }
3282
3289
  },
3283
3290
  () => {
3284
- rejectInvoke(send, id);
3291
+ rejectInvoke(send, id, validation.request);
3285
3292
  }
3286
3293
  ).catch(() => {
3287
3294
  });
@@ -3368,128 +3375,134 @@ function candidatesFor(catalog, archetype, defaultHandler) {
3368
3375
  ...entry.title === void 0 ? {} : { title: entry.title },
3369
3376
  actions: [...support.actions],
3370
3377
  conventions: [...support.conventions],
3371
- contracts: support.contracts.map((contract) => ({
3372
- convention: contract.convention,
3373
- ...contract.eventKinds === void 0 ? {} : { eventKinds: [...contract.eventKinds] }
3374
- })),
3375
3378
  ...entry.dTag === defaultHandler ? { isDefault: true } : {}
3376
3379
  });
3377
3380
  }
3378
3381
  return candidates;
3379
3382
  }
3380
- function reject(error) {
3381
- return { ok: false, error };
3383
+ function reject(request, error) {
3384
+ return {
3385
+ ok: false,
3386
+ archetype: request.archetype,
3387
+ action: request.action ?? "open",
3388
+ handled: false,
3389
+ error
3390
+ };
3391
+ }
3392
+ async function availabilityForCatalog(options, archetype) {
3393
+ const catalog = await options.loadCatalog();
3394
+ const defaultHandler = options.getDefaultHandler?.(archetype);
3395
+ const candidates = candidatesFor(catalog, archetype, defaultHandler);
3396
+ return {
3397
+ archetype,
3398
+ available: candidates.length > 0,
3399
+ candidates,
3400
+ hasDefault: defaultHandler !== void 0 && candidates.some((candidate) => candidate.dTag === defaultHandler)
3401
+ };
3402
+ }
3403
+ async function chooseCompatible(options, request, candidates, sender) {
3404
+ if (!options.chooseHandler) {
3405
+ return request.handler === "choose" ? { error: "user cancelled" } : { error: "invoke rejected" };
3406
+ }
3407
+ const picked = await options.chooseHandler(request.archetype, candidates, sender);
3408
+ if (picked === void 0) return { error: "user cancelled" };
3409
+ const candidate = candidates.find((item) => item.dTag === picked);
3410
+ return candidate ? { candidate } : { error: "invoke rejected" };
3411
+ }
3412
+ async function pickHandler(options, request, compatible, sender) {
3413
+ const preference = request.handler;
3414
+ if (typeof preference === "string" && preference !== "default" && preference !== "choose") {
3415
+ const candidate = compatible.find((item) => item.dTag === preference);
3416
+ if (!candidate || !options.authorizeExplicitHandler) return { error: "invoke rejected" };
3417
+ let authorized = false;
3418
+ try {
3419
+ authorized = await options.authorizeExplicitHandler(
3420
+ sender,
3421
+ preference,
3422
+ request,
3423
+ candidate
3424
+ );
3425
+ } catch {
3426
+ return { error: "invoke rejected" };
3427
+ }
3428
+ return authorized ? { candidate } : { error: "invoke rejected" };
3429
+ }
3430
+ if (preference === "choose") {
3431
+ return chooseCompatible(options, request, compatible, sender);
3432
+ }
3433
+ const defaultCandidate = compatible.find((candidate) => candidate.isDefault === true);
3434
+ if (preference === "default") {
3435
+ return defaultCandidate ? { candidate: defaultCandidate } : { error: "invoke rejected" };
3436
+ }
3437
+ if (defaultCandidate) return { candidate: defaultCandidate };
3438
+ if (compatible.length === 1) return { candidate: compatible[0] };
3439
+ return chooseCompatible(options, request, compatible, sender);
3440
+ }
3441
+ async function invokeFromCatalog(options, request, context) {
3442
+ const catalog = await options.loadCatalog();
3443
+ const defaultHandler = options.getDefaultHandler?.(request.archetype);
3444
+ const candidates = candidatesFor(catalog, request.archetype, defaultHandler);
3445
+ if (candidates.length === 0) return reject(request, "no handler");
3446
+ const action = request.action ?? "open";
3447
+ const actionCompatible = candidates.filter((candidate) => candidate.actions.includes(action));
3448
+ if (actionCompatible.length === 0) return reject(request, "unsupported action");
3449
+ const compatible = request.convention === void 0 ? actionCompatible : actionCompatible.filter((candidate) => candidate.conventions.includes(request.convention));
3450
+ if (compatible.length === 0) return reject(request, "unsupported convention");
3451
+ const sender = context.sender;
3452
+ if (typeof sender !== "string" || sender.length === 0) {
3453
+ return reject(request, "invoke rejected");
3454
+ }
3455
+ const selected = await pickHandler(options, request, compatible, sender);
3456
+ if ("error" in selected) return reject(request, selected.error);
3457
+ const convention = request.convention ?? selected.candidate.conventions.find((value) => {
3458
+ const match = /^napplet:[^/?#\s]+\/([^/?#\s]+)$/.exec(value);
3459
+ return match?.[1] === action;
3460
+ });
3461
+ if (!convention) return reject(request, "unsupported convention");
3462
+ const behavior = request.behavior === void 0 ? void 0 : Object.freeze({
3463
+ ...request.behavior.focus === void 0 ? {} : { focus: request.behavior.focus },
3464
+ ...request.behavior.newWindow === void 0 ? {} : { newWindow: request.behavior.newWindow },
3465
+ ...request.behavior.reuse === void 0 ? {} : { reuse: request.behavior.reuse }
3466
+ });
3467
+ const params = Object.freeze({
3468
+ handler: selected.candidate.dTag,
3469
+ sender,
3470
+ archetype: request.archetype,
3471
+ action,
3472
+ convention,
3473
+ ...request.payload === void 0 ? {} : { payload: request.payload },
3474
+ ...behavior === void 0 ? {} : { behavior }
3475
+ });
3476
+ let target;
3477
+ try {
3478
+ target = await options.targets.dispatch(params);
3479
+ } catch {
3480
+ return reject(request, "invoke failed");
3481
+ }
3482
+ if (!target || typeof target.windowId !== "string" || target.windowId.length === 0) {
3483
+ return reject(request, "invoke failed");
3484
+ }
3485
+ return {
3486
+ ok: true,
3487
+ archetype: request.archetype,
3488
+ action,
3489
+ handled: true,
3490
+ handler: selected.candidate.dTag,
3491
+ windowId: target.windowId,
3492
+ convention
3493
+ };
3382
3494
  }
3383
3495
  function createCatalogIntentResolver(options) {
3384
3496
  if (!options || typeof options.loadCatalog !== "function") {
3385
3497
  throw new Error("createCatalogIntentResolver: options.loadCatalog is required");
3386
3498
  }
3387
- if (!options.targets || typeof options.targets.retain !== "function") {
3499
+ if (!options.targets || typeof options.targets.dispatch !== "function") {
3388
3500
  throw new Error("createCatalogIntentResolver: options.targets is required");
3389
3501
  }
3390
- const {
3391
- loadCatalog,
3392
- targets,
3393
- getDefaultHandler,
3394
- chooseHandler,
3395
- authorizeExplicitHandler
3396
- } = options;
3397
3502
  const listeners = /* @__PURE__ */ new Set();
3398
- async function availabilityFor(archetype) {
3399
- const catalog = await loadCatalog();
3400
- const defaultHandler = getDefaultHandler?.(archetype);
3401
- const candidates = candidatesFor(catalog, archetype, defaultHandler);
3402
- return {
3403
- archetype,
3404
- available: candidates.length > 0,
3405
- candidates,
3406
- hasDefault: defaultHandler !== void 0 && candidates.some((candidate) => candidate.dTag === defaultHandler)
3407
- };
3408
- }
3409
- async function chooseCompatible(request, candidates, sender) {
3410
- if (!chooseHandler) {
3411
- return request.handler === "choose" ? { error: "user cancelled" } : { error: "invoke rejected" };
3412
- }
3413
- const picked = await chooseHandler(request.archetype, candidates, sender);
3414
- if (picked === void 0) return { error: "user cancelled" };
3415
- const candidate = candidates.find((item) => item.dTag === picked);
3416
- return candidate ? { candidate } : { error: "invoke rejected" };
3417
- }
3418
- async function pickHandler(request, compatible, sender) {
3419
- const preference = request.handler;
3420
- if (typeof preference === "string" && preference !== "default" && preference !== "choose") {
3421
- const candidate = compatible.find((item) => item.dTag === preference);
3422
- if (!candidate || !authorizeExplicitHandler) return { error: "invoke rejected" };
3423
- let authorized = false;
3424
- try {
3425
- authorized = await authorizeExplicitHandler(sender, preference, request, candidate);
3426
- } catch {
3427
- return { error: "invoke rejected" };
3428
- }
3429
- return authorized ? { candidate } : { error: "invoke rejected" };
3430
- }
3431
- if (preference === "choose") {
3432
- return chooseCompatible(request, compatible, sender);
3433
- }
3434
- const defaultCandidate = compatible.find((candidate) => candidate.isDefault === true);
3435
- if (preference === "default") {
3436
- return defaultCandidate ? { candidate: defaultCandidate } : { error: "invoke rejected" };
3437
- }
3438
- if (defaultCandidate) return { candidate: defaultCandidate };
3439
- if (compatible.length === 1) return { candidate: compatible[0] };
3440
- return chooseCompatible(request, compatible, sender);
3441
- }
3442
- async function invoke(request, context) {
3443
- const catalog = await loadCatalog();
3444
- const defaultHandler = getDefaultHandler?.(request.archetype);
3445
- const candidates = candidatesFor(catalog, request.archetype, defaultHandler);
3446
- if (candidates.length === 0) return { result: reject("no handler") };
3447
- const compatible = candidates.filter((candidate) => candidate.contracts.some((contract) => contract.convention === request.convention));
3448
- if (compatible.length === 0) return { result: reject("unsupported convention") };
3449
- const sender = context.sender;
3450
- if (typeof sender !== "string" || sender.length === 0) {
3451
- return { result: reject("invoke rejected") };
3452
- }
3453
- const selected = await pickHandler(request, compatible, sender);
3454
- if ("error" in selected) return { result: reject(selected.error) };
3455
- const delivery = Object.freeze({
3456
- sender,
3457
- archetype: request.archetype,
3458
- action: request.action,
3459
- convention: request.convention,
3460
- ...request.payload === void 0 ? {} : { payload: request.payload }
3461
- });
3462
- const behavior = request.behavior === void 0 ? void 0 : Object.freeze({
3463
- ...request.behavior.focus === void 0 ? {} : { focus: request.behavior.focus },
3464
- ...request.behavior.reuse === void 0 ? {} : { reuse: request.behavior.reuse }
3465
- });
3466
- const params = Object.freeze({
3467
- handler: selected.candidate.dTag,
3468
- delivery,
3469
- ...behavior === void 0 ? {} : { behavior }
3470
- });
3471
- let retained;
3472
- try {
3473
- retained = await targets.retain(params);
3474
- } catch {
3475
- return { result: reject("invoke rejected") };
3476
- }
3477
- if (!retained || typeof retained.start !== "function") {
3478
- return { result: reject("invoke rejected") };
3479
- }
3480
- return {
3481
- result: {
3482
- ok: true,
3483
- archetype: request.archetype,
3484
- action: request.action,
3485
- convention: request.convention,
3486
- handler: selected.candidate.dTag
3487
- },
3488
- retained
3489
- };
3490
- }
3503
+ const availabilityFor = (archetype) => availabilityForCatalog(options, archetype);
3491
3504
  async function handlers() {
3492
- const catalog = await loadCatalog();
3505
+ const catalog = await options.loadCatalog();
3493
3506
  const archetypes = /* @__PURE__ */ new Set();
3494
3507
  for (const entry of catalog) {
3495
3508
  for (const slug of Object.keys(entry.archetypes)) archetypes.add(slug);
@@ -3497,7 +3510,7 @@ function createCatalogIntentResolver(options) {
3497
3510
  return Promise.all([...archetypes].map((archetype) => availabilityFor(archetype)));
3498
3511
  }
3499
3512
  return {
3500
- invoke,
3513
+ invoke: (request, context) => invokeFromCatalog(options, request, context),
3501
3514
  available: availabilityFor,
3502
3515
  handlers,
3503
3516
  onChanged(listener) {
@@ -3526,21 +3539,12 @@ function actionFromConvention(slug, convention) {
3526
3539
  }
3527
3540
  function manifestToIntentCatalogEntry(manifest) {
3528
3541
  const archetypes = {};
3529
- for (const { slug, convention, eventKinds } of manifest.archetypes) {
3542
+ for (const { slug, convention } of manifest.archetypes) {
3530
3543
  const action = actionFromConvention(slug, convention);
3531
- if (eventKinds?.some((kind) => !Number.isSafeInteger(kind) || kind < 0)) {
3532
- throw new TypeError("manifest archetype event kinds must be unsigned safe integers");
3533
- }
3534
3544
  const support = archetypes[slug] ??= {
3535
3545
  actions: [],
3536
- conventions: [],
3537
- contracts: []
3538
- };
3539
- const contract = {
3540
- convention,
3541
- ...eventKinds === void 0 || eventKinds.length === 0 ? {} : { eventKinds: [...eventKinds] }
3546
+ conventions: []
3542
3547
  };
3543
- support.contracts.push(contract);
3544
3548
  if (!support.actions.includes(action)) support.actions.push(action);
3545
3549
  if (!support.conventions.includes(convention)) support.conventions.push(convention);
3546
3550
  }