@things-factory/attachment-base 10.1.111 → 10.1.112

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.
Files changed (40) hide show
  1. package/dist-server/attachment-access.d.ts +21 -0
  2. package/dist-server/attachment-access.js +111 -0
  3. package/dist-server/attachment-access.js.map +1 -0
  4. package/dist-server/attachment-const.d.ts +5 -0
  5. package/dist-server/attachment-const.js +6 -1
  6. package/dist-server/attachment-const.js.map +1 -1
  7. package/dist-server/index.d.ts +1 -0
  8. package/dist-server/index.js +1 -0
  9. package/dist-server/index.js.map +1 -1
  10. package/dist-server/routes.js +59 -17
  11. package/dist-server/routes.js.map +1 -1
  12. package/dist-server/service/attachment/index.d.ts +1 -1
  13. package/dist-server/service/attachment-share/attachment-share-mutation.d.ts +11 -0
  14. package/dist-server/service/attachment-share/attachment-share-mutation.js +87 -0
  15. package/dist-server/service/attachment-share/attachment-share-mutation.js.map +1 -0
  16. package/dist-server/service/attachment-share/attachment-share-rule.d.ts +16 -0
  17. package/dist-server/service/attachment-share/attachment-share-rule.js +41 -0
  18. package/dist-server/service/attachment-share/attachment-share-rule.js.map +1 -0
  19. package/dist-server/service/attachment-share/attachment-share.d.ts +15 -0
  20. package/dist-server/service/attachment-share/attachment-share.js +72 -0
  21. package/dist-server/service/attachment-share/attachment-share.js.map +1 -0
  22. package/dist-server/service/attachment-share/index.d.ts +4 -0
  23. package/dist-server/service/attachment-share/index.js +8 -0
  24. package/dist-server/service/attachment-share/index.js.map +1 -0
  25. package/dist-server/service/index.d.ts +4 -2
  26. package/dist-server/service/index.js +7 -2
  27. package/dist-server/service/index.js.map +1 -1
  28. package/dist-server/storage-azure-blob.js +1 -1
  29. package/dist-server/storage-azure-blob.js.map +1 -1
  30. package/dist-server/storage-s3.js +1 -1
  31. package/dist-server/storage-s3.js.map +1 -1
  32. package/dist-server/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +2 -2
  34. package/tests/attachment-access.test.ts +93 -0
  35. package/tests/attachment-route.test.ts +88 -0
  36. package/translations/en.json +3 -2
  37. package/translations/ja.json +2 -1
  38. package/translations/ko.json +3 -2
  39. package/translations/ms.json +2 -1
  40. package/translations/zh.json +2 -1
@@ -0,0 +1,21 @@
1
+ import { Attachment } from './service/attachment/attachment';
2
+ export type AttachmentReader = (attachment: Attachment, context: any) => boolean | Promise<boolean>;
3
+ /** The module owning `refType` says who may read its attachments. Same key replaces, `null` takes it back. */
4
+ export declare function registerAttachmentReader(refType: string, reader: AttachmentReader | null): void;
5
+ export declare function attachmentReaderOf(refType: string): AttachmentReader | undefined;
6
+ /**
7
+ * Attachments of people, money and regulated records - closed unless their module registers a reader.
8
+ * From the inventory of 2026-10-02 (docs/design/attachment-access.md §5).
9
+ */
10
+ export declare const SENSITIVE_REF_TYPES: ReadonlySet<string>;
11
+ /** Attachments any member of the domain may read when no reader is registered. */
12
+ export declare const ORDINARY_REF_TYPES: ReadonlySet<string>;
13
+ /** The Attachment id a storage path carries - `<id>` or `<id>.<ext>`. Anything else is not an attachment path. */
14
+ export declare function attachmentIdOf(path: string): string | null;
15
+ /** Whether the rules let the request read this row - pure, so it is tested without a server. */
16
+ export declare function mayRead(attachment: Attachment, context: any): Promise<boolean>;
17
+ /**
18
+ * The attachment a path names, if this request may read it - one answer (`undefined`) for "none", "not this domain's",
19
+ * "not yours" and "a file with no row", so the answer never says what exists.
20
+ */
21
+ export declare function readableAttachment(path: string, context: any): Promise<Attachment | undefined>;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ORDINARY_REF_TYPES = exports.SENSITIVE_REF_TYPES = void 0;
4
+ exports.registerAttachmentReader = registerAttachmentReader;
5
+ exports.attachmentReaderOf = attachmentReaderOf;
6
+ exports.attachmentIdOf = attachmentIdOf;
7
+ exports.mayRead = mayRead;
8
+ exports.readableAttachment = readableAttachment;
9
+ const shell_1 = require("@things-factory/shell");
10
+ const attachment_1 = require("./service/attachment/attachment");
11
+ const readers = new Map();
12
+ /** The module owning `refType` says who may read its attachments. Same key replaces, `null` takes it back. */
13
+ function registerAttachmentReader(refType, reader) {
14
+ if (reader)
15
+ readers.set(refType, reader);
16
+ else
17
+ readers.delete(refType);
18
+ }
19
+ function attachmentReaderOf(refType) {
20
+ return readers.get(refType ?? '');
21
+ }
22
+ /**
23
+ * Attachments of people, money and regulated records - closed unless their module registers a reader.
24
+ * From the inventory of 2026-10-02 (docs/design/attachment-access.md §5).
25
+ */
26
+ exports.SENSITIVE_REF_TYPES = new Set([
27
+ /* people */
28
+ 'Employee',
29
+ 'Contact',
30
+ /* money */
31
+ 'FinancialInstitution',
32
+ 'TaxAuditResult',
33
+ 'TaxInvoiceItem',
34
+ 'Quotation',
35
+ 'ExpenseClaimItem',
36
+ 'Office Financial Statement',
37
+ 'Office Share Register',
38
+ 'OfficeContractDocument',
39
+ 'OfficeContractDocumentRemoved',
40
+ /* regulated - measurement, reports, corporate documents */
41
+ 'DataSample',
42
+ 'DataSet-report-template',
43
+ 'Office Articles',
44
+ /* content decided by whoever made it */
45
+ 'headless-pdf-save',
46
+ 'chat-session'
47
+ ]);
48
+ /** Attachments any member of the domain may read when no reader is registered. */
49
+ exports.ORDINARY_REF_TYPES = new Set([
50
+ /* the generic upload (attachment list, image selector, image uploader) - board and page images */
51
+ '',
52
+ 'Department',
53
+ 'Product',
54
+ 'WorkCenter',
55
+ 'NotificationRule',
56
+ 'DataKeySet',
57
+ 'Operation',
58
+ 'Kpi',
59
+ 'KpiMetric',
60
+ 'Process',
61
+ 'Rack',
62
+ 'Font',
63
+ 'Activity',
64
+ 'ActivityTemplate',
65
+ 'activity-template',
66
+ 'figure-thumbnail',
67
+ 'board-import',
68
+ 'twin-space-representation'
69
+ ]);
70
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
71
+ /** The Attachment id a storage path carries - `<id>` or `<id>.<ext>`. Anything else is not an attachment path. */
72
+ function attachmentIdOf(path) {
73
+ const name = String(path || '');
74
+ if (!name || name.includes('/') || name.includes('\\'))
75
+ return null;
76
+ const id = name.split('.')[0];
77
+ return UUID.test(id) ? id : null;
78
+ }
79
+ /** Whether the rules let the request read this row - pure, so it is tested without a server. */
80
+ async function mayRead(attachment, context) {
81
+ const domainId = context?.state?.domain?.id;
82
+ if (!attachment || !domainId || attachment.domainId !== domainId)
83
+ return false;
84
+ if (!context?.state?.user?.id)
85
+ return false;
86
+ const reader = attachmentReaderOf(attachment.refType);
87
+ if (reader) {
88
+ try {
89
+ return (await reader(attachment, context)) === true;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ return exports.ORDINARY_REF_TYPES.has(attachment.refType ?? '');
96
+ }
97
+ /**
98
+ * The attachment a path names, if this request may read it - one answer (`undefined`) for "none", "not this domain's",
99
+ * "not yours" and "a file with no row", so the answer never says what exists.
100
+ */
101
+ async function readableAttachment(path, context) {
102
+ const id = attachmentIdOf(path);
103
+ const domainId = context?.state?.domain?.id;
104
+ if (!id || !domainId)
105
+ return undefined;
106
+ const row = await (0, shell_1.getRepository)(attachment_1.Attachment).findOne({ where: { id, domain: { id: domainId } } });
107
+ if (!row || row.path !== path)
108
+ return undefined;
109
+ return (await mayRead(row, context)) ? row : undefined;
110
+ }
111
+ //# sourceMappingURL=attachment-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-access.js","sourceRoot":"","sources":["../server/attachment-access.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { Attachment } from './service/attachment/attachment'\n\n/*\n * Who may read an attachment - ADR-0106 decision 2.\n *\n * An attachment is a record of one domain. It is found by the id its storage path carries (`<id>.<ext>` - the\n * Attachment PK) and must belong to the request's domain. The module that owns the record it hangs on (`refType`)\n * says who among that domain's people may read it; attachment-base does not open a parent's attachments for a child -\n * a module that wants that says so in its own reader.\n *\n * A `refType` with no reader is read by any member of the domain only when it is on ORDINARY_REF_TYPES. Everything\n * else - the people, money and regulated records on SENSITIVE_REF_TYPES, and any `refType` on neither list - stays\n * closed until its module registers a reader. A new `refType` therefore cannot open by being forgotten.\n */\n\nexport type AttachmentReader = (attachment: Attachment, context: any) => boolean | Promise<boolean>\n\nconst readers = new Map<string, AttachmentReader>()\n\n/** The module owning `refType` says who may read its attachments. Same key replaces, `null` takes it back. */\nexport function registerAttachmentReader(refType: string, reader: AttachmentReader | null): void {\n if (reader) readers.set(refType, reader)\n else readers.delete(refType)\n}\n\nexport function attachmentReaderOf(refType: string): AttachmentReader | undefined {\n return readers.get(refType ?? '')\n}\n\n/**\n * Attachments of people, money and regulated records - closed unless their module registers a reader.\n * From the inventory of 2026-10-02 (docs/design/attachment-access.md §5).\n */\nexport const SENSITIVE_REF_TYPES: ReadonlySet<string> = new Set<string>([\n /* people */\n 'Employee',\n 'Contact',\n /* money */\n 'FinancialInstitution',\n 'TaxAuditResult',\n 'TaxInvoiceItem',\n 'Quotation',\n 'ExpenseClaimItem',\n 'Office Financial Statement',\n 'Office Share Register',\n 'OfficeContractDocument',\n 'OfficeContractDocumentRemoved',\n /* regulated - measurement, reports, corporate documents */\n 'DataSample',\n 'DataSet-report-template',\n 'Office Articles',\n /* content decided by whoever made it */\n 'headless-pdf-save',\n 'chat-session'\n])\n\n/** Attachments any member of the domain may read when no reader is registered. */\nexport const ORDINARY_REF_TYPES: ReadonlySet<string> = new Set<string>([\n /* the generic upload (attachment list, image selector, image uploader) - board and page images */\n '',\n 'Department',\n 'Product',\n 'WorkCenter',\n 'NotificationRule',\n 'DataKeySet',\n 'Operation',\n 'Kpi',\n 'KpiMetric',\n 'Process',\n 'Rack',\n 'Font',\n 'Activity',\n 'ActivityTemplate',\n 'activity-template',\n 'figure-thumbnail',\n 'board-import',\n 'twin-space-representation'\n])\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/** The Attachment id a storage path carries - `<id>` or `<id>.<ext>`. Anything else is not an attachment path. */\nexport function attachmentIdOf(path: string): string | null {\n const name = String(path || '')\n if (!name || name.includes('/') || name.includes('\\\\')) return null\n const id = name.split('.')[0]\n return UUID.test(id) ? id : null\n}\n\n/** Whether the rules let the request read this row - pure, so it is tested without a server. */\nexport async function mayRead(attachment: Attachment, context: any): Promise<boolean> {\n const domainId = context?.state?.domain?.id\n if (!attachment || !domainId || attachment.domainId !== domainId) return false\n if (!context?.state?.user?.id) return false\n\n const reader = attachmentReaderOf(attachment.refType)\n if (reader) {\n try {\n return (await reader(attachment, context)) === true\n } catch {\n return false\n }\n }\n return ORDINARY_REF_TYPES.has(attachment.refType ?? '')\n}\n\n/**\n * The attachment a path names, if this request may read it - one answer (`undefined`) for \"none\", \"not this domain's\",\n * \"not yours\" and \"a file with no row\", so the answer never says what exists.\n */\nexport async function readableAttachment(path: string, context: any): Promise<Attachment | undefined> {\n const id = attachmentIdOf(path)\n const domainId = context?.state?.domain?.id\n if (!id || !domainId) return undefined\n\n const row = await getRepository(Attachment).findOne({ where: { id, domain: { id: domainId } } as any })\n if (!row || row.path !== path) return undefined\n return (await mayRead(row, context)) ? row : undefined\n}\n"]}
@@ -1,3 +1,8 @@
1
1
  export declare var STORAGE: any;
2
2
  export declare var AWBSTORAGE: any;
3
3
  export declare const ATTACHMENT_PATH: string;
4
+ /**
5
+ * How long a browser may keep an attachment - and only the browser. `private`: an attachment belongs to one domain's
6
+ * people, so a shared proxy or CDN must not keep it (ADR-0106). Every storage sets this one value.
7
+ */
8
+ export declare const ATTACHMENT_CACHE_CONTROL = "private, max-age=31536000";
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ATTACHMENT_PATH = exports.AWBSTORAGE = exports.STORAGE = void 0;
3
+ exports.ATTACHMENT_CACHE_CONTROL = exports.ATTACHMENT_PATH = exports.AWBSTORAGE = exports.STORAGE = void 0;
4
4
  const env_1 = require("@things-factory/env");
5
5
  exports.STORAGE = env_1.config.get('storage');
6
6
  exports.AWBSTORAGE = env_1.config.get('awbFileStorage');
7
7
  exports.ATTACHMENT_PATH = env_1.config.get('attachmentPath', 'attachment');
8
+ /**
9
+ * How long a browser may keep an attachment - and only the browser. `private`: an attachment belongs to one domain's
10
+ * people, so a shared proxy or CDN must not keep it (ADR-0106). Every storage sets this one value.
11
+ */
12
+ exports.ATTACHMENT_CACHE_CONTROL = 'private, max-age=31536000';
8
13
  //# sourceMappingURL=attachment-const.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-const.js","sourceRoot":"","sources":["../server/attachment-const.ts"],"names":[],"mappings":";;;AAAA,6CAA4C;AAEjC,QAAA,OAAO,GAAQ,YAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;AACpC,QAAA,UAAU,GAAQ,YAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;AAC5C,QAAA,eAAe,GAAW,YAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAA","sourcesContent":["import { config } from '@things-factory/env'\n\nexport var STORAGE: any = config.get('storage')\nexport var AWBSTORAGE: any = config.get('awbFileStorage')\nexport const ATTACHMENT_PATH: string = config.get('attachmentPath', 'attachment')\n"]}
1
+ {"version":3,"file":"attachment-const.js","sourceRoot":"","sources":["../server/attachment-const.ts"],"names":[],"mappings":";;;AAAA,6CAA4C;AAEjC,QAAA,OAAO,GAAQ,YAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;AACpC,QAAA,UAAU,GAAQ,YAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;AAC5C,QAAA,eAAe,GAAW,YAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAA;AAEjF;;;GAGG;AACU,QAAA,wBAAwB,GAAG,2BAA2B,CAAA","sourcesContent":["import { config } from '@things-factory/env'\n\nexport var STORAGE: any = config.get('storage')\nexport var AWBSTORAGE: any = config.get('awbFileStorage')\nexport const ATTACHMENT_PATH: string = config.get('attachmentPath', 'attachment')\n\n/**\n * How long a browser may keep an attachment - and only the browser. `private`: an attachment belongs to one domain's\n * people, so a shared proxy or CDN must not keep it (ADR-0106). Every storage sets this one value.\n */\nexport const ATTACHMENT_CACHE_CONTROL = 'private, max-age=31536000'\n"]}
@@ -1,4 +1,5 @@
1
1
  export * from './attachment-const';
2
+ export * from './attachment-access';
2
3
  export * from './service';
3
4
  export { createAttachment, createAttachments, deleteAttachment, deleteAttachmentsByRef, multipleUpload, singleUpload } from './service/attachment/attachment-mutation';
4
5
  import './routes';
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.singleUpload = exports.multipleUpload = exports.deleteAttachmentsByRef = exports.deleteAttachment = exports.createAttachments = exports.createAttachment = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  tslib_1.__exportStar(require("./attachment-const"), exports);
6
+ tslib_1.__exportStar(require("./attachment-access"), exports);
6
7
  tslib_1.__exportStar(require("./service"), exports);
7
8
  var attachment_mutation_1 = require("./service/attachment/attachment-mutation");
8
9
  Object.defineProperty(exports, "createAttachment", { enumerable: true, get: function () { return attachment_mutation_1.createAttachment; } });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;;AAAA,6DAAkC;AAClC,oDAAyB;AACzB,gFAOiD;AAN/C,uHAAA,gBAAgB,OAAA;AAChB,wHAAA,iBAAiB,OAAA;AACjB,uHAAA,gBAAgB,OAAA;AAChB,6HAAA,sBAAsB,OAAA;AACtB,qHAAA,cAAc,OAAA;AACd,mHAAA,YAAY,OAAA;AAGd,oBAAiB;AAEjB,6DAAkC;AAClC,iDAAsB;AAEtB,mDAAqD;AAErD,OAAO,CAAC,EAAE,CAAC,wBAA+B,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAO,EAAE,EAAE;IACjF,qDAAqD;IACrD,IAAA,mCAAmB,GAAE;SAClB,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;SACnD,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC,CAAA;AACpE,CAAC,CAAC,CAAA","sourcesContent":["export * from './attachment-const'\nexport * from './service'\nexport {\n createAttachment,\n createAttachments,\n deleteAttachment,\n deleteAttachmentsByRef,\n multipleUpload,\n singleUpload\n} from './service/attachment/attachment-mutation'\n\nimport './routes'\n\nexport * from './attachment-const'\nexport * from './util'\n\nimport { normalizeNamesToNFC } from './nfc-normalize'\n\nprocess.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {\n /* 이 코드는 기존 첨부파일의 NFC 정규화를 강제로 실행하기 위해서 임시로 작성되었다. */\n normalizeNamesToNFC()\n .then(() => console.log('Normalization completed.'))\n .catch(err => console.error('Error during normalization:', err))\n})\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;;AAAA,6DAAkC;AAClC,8DAAmC;AACnC,oDAAyB;AACzB,gFAOiD;AAN/C,uHAAA,gBAAgB,OAAA;AAChB,wHAAA,iBAAiB,OAAA;AACjB,uHAAA,gBAAgB,OAAA;AAChB,6HAAA,sBAAsB,OAAA;AACtB,qHAAA,cAAc,OAAA;AACd,mHAAA,YAAY,OAAA;AAGd,oBAAiB;AAEjB,6DAAkC;AAClC,iDAAsB;AAEtB,mDAAqD;AAErD,OAAO,CAAC,EAAE,CAAC,wBAA+B,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAO,EAAE,EAAE;IACjF,qDAAqD;IACrD,IAAA,mCAAmB,GAAE;SAClB,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;SACnD,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC,CAAA;AACpE,CAAC,CAAC,CAAA","sourcesContent":["export * from './attachment-const'\nexport * from './attachment-access'\nexport * from './service'\nexport {\n createAttachment,\n createAttachments,\n deleteAttachment,\n deleteAttachmentsByRef,\n multipleUpload,\n singleUpload\n} from './service/attachment/attachment-mutation'\n\nimport './routes'\n\nexport * from './attachment-const'\nexport * from './util'\n\nimport { normalizeNamesToNFC } from './nfc-normalize'\n\nprocess.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {\n /* 이 코드는 기존 첨부파일의 NFC 정규화를 강제로 실행하기 위해서 임시로 작성되었다. */\n normalizeNamesToNFC()\n .then(() => console.log('Normalization completed.'))\n .catch(err => console.error('Error during normalization:', err))\n})\n"]}
@@ -4,25 +4,67 @@ require("./storage-file");
4
4
  require("./storage-s3");
5
5
  require("./storage-database");
6
6
  require("./storage-azure-blob");
7
+ const env_1 = require("@things-factory/env");
8
+ const shell_1 = require("@things-factory/shell");
9
+ const auth_base_1 = require("@things-factory/auth-base");
10
+ const attachment_access_1 = require("./attachment-access");
7
11
  const attachment_const_1 = require("./attachment-const");
8
- const multer = require('@koa/multer');
9
- const upload = multer(); // note you can pass `multer` options here
10
- const { Readable } = require('stream');
11
- // process.on('bootstrap-module-domain-private-route' as any, (app, routes) => {
12
+ const attachment_1 = require("./service/attachment/attachment");
13
+ const attachment_share_1 = require("./service/attachment-share/attachment-share");
14
+ const attachment_share_rule_1 = require("./service/attachment-share/attachment-share-rule");
15
+ /*
16
+ * Where attachments are served (ADR-0106).
17
+ *
18
+ * - The anonymous `POST /attachment` upload is gone: it stored a file with no row and no domain, and nothing called
19
+ * it - every upload goes through GraphQL `createAttachment`. Every response is `private`: no shared cache keeps one.
20
+ * - `attachment/requireDomain` (decision 2) moves `GET /attachment/:path` behind sign-in, the attachment's domain and
21
+ * its module's reader. It stays off until the consumers that load attachments without a session are fixed
22
+ * (decision 4) - turned on with one of them left, label printing would come out without its images.
23
+ * - The domain-addressed read (`/<domain-type>/:domain/attachment/:path`) and the share link are served from now on.
24
+ */
25
+ const requireDomain = () => env_1.config.get('attachment', {})?.requireDomain === true;
26
+ const isPathBaseDomain = !env_1.config.get('subdomain');
27
+ async function sendReadable(context, next) {
28
+ const row = await (0, attachment_access_1.readableAttachment)(context.params.attachment, context);
29
+ if (!row) {
30
+ /* One answer for "none", "not this domain's" and "not yours". */
31
+ context.status = 404;
32
+ return;
33
+ }
34
+ context.set('Cache-Control', attachment_const_1.ATTACHMENT_CACHE_CONTROL);
35
+ await attachment_const_1.STORAGE.sendFile(context, row.path, next);
36
+ }
12
37
  process.on('bootstrap-module-global-public-route', (app, routes) => {
13
- // TODO make this secure
14
- routes.get(`/${attachment_const_1.ATTACHMENT_PATH}/:attachment`, async (context, next) => {
15
- context.set('Cache-Control', 'public, max-age=31536000'); // 캐시 기간 설정 : 1년
16
- context.set('Expires', new Date(Date.now() + 31536000000).toUTCString()); // 캐시 만료일 설정
17
- await attachment_const_1.STORAGE.sendFile(context, context.params.attachment, next);
18
- });
19
- routes.post(`/${attachment_const_1.ATTACHMENT_PATH}`, upload.any(), async (context, next) => {
20
- const files = context.files;
21
- const result = await Promise.all(files.map(file => attachment_const_1.STORAGE.uploadFile({ file, context })));
22
- context.status = 200;
23
- // Support < IE 10 browser
24
- context.set('Content-Type', 'text/html;charset=UTF-8');
25
- context.body = JSON.stringify(result);
38
+ if (!requireDomain()) {
39
+ routes.get(`/${attachment_const_1.ATTACHMENT_PATH}/:attachment`, async (context, next) => {
40
+ context.set('Cache-Control', attachment_const_1.ATTACHMENT_CACHE_CONTROL);
41
+ context.set('Expires', new Date(Date.now() + 31536000000).toUTCString()); // 캐시 만료일 설정
42
+ await attachment_const_1.STORAGE.sendFile(context, context.params.attachment, next);
43
+ });
44
+ }
45
+ /* A link handed outside - opens without a session while its signature, expiry and record say so (decision 3). */
46
+ routes.get(`/${attachment_share_rule_1.ATTACHMENT_SHARE_PATH}/:share/:signature`, async (context, next) => {
47
+ const share = await (0, shell_1.getRepository)(attachment_share_1.AttachmentShare).findOne({ where: { id: context.params.share } });
48
+ const now = new Date();
49
+ if (!share || !(0, attachment_share_rule_1.shareOpens)(auth_base_1.SECRET, share, context.params.signature, now)) {
50
+ context.status = 404;
51
+ return;
52
+ }
53
+ const row = await (0, shell_1.getRepository)(attachment_1.Attachment).findOne({ where: { id: share.attachmentId, domain: { id: share.domainId } } });
54
+ if (!row) {
55
+ context.status = 404;
56
+ return;
57
+ }
58
+ /* Never kept by anyone: the link can be taken back, and a copy in a cache would outlive that. */
59
+ context.set('Cache-Control', 'private, no-store');
60
+ context.set('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(row.name || row.path)}`);
61
+ await attachment_const_1.STORAGE.sendFile(context, row.path, next);
26
62
  });
27
63
  });
64
+ process.on('bootstrap-module-domain-private-route', (app, routes) => {
65
+ if (isPathBaseDomain)
66
+ routes.get(`/${(0, shell_1.getRoutePrefixForDomainType)()}/:domain/${attachment_const_1.ATTACHMENT_PATH}/:attachment`, sendReadable);
67
+ if (requireDomain())
68
+ routes.get(`/${attachment_const_1.ATTACHMENT_PATH}/:attachment`, sendReadable);
69
+ });
28
70
  //# sourceMappingURL=routes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../server/routes.ts"],"names":[],"mappings":";;AAAA,0BAAuB;AACvB,wBAAqB;AACrB,8BAA2B;AAC3B,gCAA6B;AAE7B,yDAA6D;AAE7D,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;AACrC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAA,CAAC,0CAA0C;AAElE,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEtC,gFAAgF;AAChF,OAAO,CAAC,EAAE,CAAC,sCAA6C,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;IACxE,wBAAwB;IACxB,MAAM,CAAC,GAAG,CAAC,IAAI,kCAAe,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QACpE,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAA,CAAC,gBAAgB;QACzE,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC,CAAA,CAAC,YAAY;QAErF,MAAM,0BAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAA;IAClE,CAAC,CAAC,CAAA;IAEF,MAAM,CAAC,IAAI,CAAC,IAAI,kCAAe,EAAE,EAAE,MAAM,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QACvE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;QAE3B,MAAM,MAAM,GAAiD,MAAM,OAAO,CAAC,GAAG,CAC5E,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,0BAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CACzD,CAAA;QAED,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,0BAA0B;QAC1B,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,yBAAyB,CAAC,CAAA;QACtD,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import './storage-file'\nimport './storage-s3'\nimport './storage-database'\nimport './storage-azure-blob'\n\nimport { ATTACHMENT_PATH, STORAGE } from './attachment-const'\n\nconst multer = require('@koa/multer')\nconst upload = multer() // note you can pass `multer` options here\n\nconst { Readable } = require('stream')\n\n// process.on('bootstrap-module-domain-private-route' as any, (app, routes) => {\nprocess.on('bootstrap-module-global-public-route' as any, (app, routes) => {\n // TODO make this secure\n routes.get(`/${ATTACHMENT_PATH}/:attachment`, async (context, next) => {\n context.set('Cache-Control', 'public, max-age=31536000') // 캐시 기간 설정 : 1년\n context.set('Expires', new Date(Date.now() + 31536000000).toUTCString()) // 캐시 만료일 설정\n\n await STORAGE.sendFile(context, context.params.attachment, next)\n })\n\n routes.post(`/${ATTACHMENT_PATH}`, upload.any(), async (context, next) => {\n const files = context.files\n\n const result: { id: string; path: string; size: number }[] = await Promise.all(\n files.map(file => STORAGE.uploadFile({ file, context }))\n )\n\n context.status = 200\n // Support < IE 10 browser\n context.set('Content-Type', 'text/html;charset=UTF-8')\n context.body = JSON.stringify(result)\n })\n})\n"]}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../server/routes.ts"],"names":[],"mappings":"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","sourcesContent":["import './storage-file'\nimport './storage-s3'\nimport './storage-database'\nimport './storage-azure-blob'\n\nimport { config } from '@things-factory/env'\nimport { getRepository, getRoutePrefixForDomainType } from '@things-factory/shell'\nimport { SECRET } from '@things-factory/auth-base'\n\nimport { readableAttachment } from './attachment-access'\nimport { ATTACHMENT_CACHE_CONTROL, ATTACHMENT_PATH, STORAGE } from './attachment-const'\nimport { Attachment } from './service/attachment/attachment'\nimport { AttachmentShare } from './service/attachment-share/attachment-share'\nimport { ATTACHMENT_SHARE_PATH, shareOpens } from './service/attachment-share/attachment-share-rule'\n\n/*\n * Where attachments are served (ADR-0106).\n *\n * - The anonymous `POST /attachment` upload is gone: it stored a file with no row and no domain, and nothing called\n * it - every upload goes through GraphQL `createAttachment`. Every response is `private`: no shared cache keeps one.\n * - `attachment/requireDomain` (decision 2) moves `GET /attachment/:path` behind sign-in, the attachment's domain and\n * its module's reader. It stays off until the consumers that load attachments without a session are fixed\n * (decision 4) - turned on with one of them left, label printing would come out without its images.\n * - The domain-addressed read (`/<domain-type>/:domain/attachment/:path`) and the share link are served from now on.\n */\nconst requireDomain = () => config.get('attachment', {})?.requireDomain === true\nconst isPathBaseDomain = !config.get('subdomain')\n\nasync function sendReadable(context: any, next: any) {\n const row = await readableAttachment(context.params.attachment, context)\n if (!row) {\n /* One answer for \"none\", \"not this domain's\" and \"not yours\". */\n context.status = 404\n return\n }\n context.set('Cache-Control', ATTACHMENT_CACHE_CONTROL)\n await STORAGE.sendFile(context, row.path, next)\n}\n\nprocess.on('bootstrap-module-global-public-route' as any, (app, routes) => {\n if (!requireDomain()) {\n routes.get(`/${ATTACHMENT_PATH}/:attachment`, async (context, next) => {\n context.set('Cache-Control', ATTACHMENT_CACHE_CONTROL)\n context.set('Expires', new Date(Date.now() + 31536000000).toUTCString()) // 캐시 만료일 설정\n\n await STORAGE.sendFile(context, context.params.attachment, next)\n })\n }\n\n /* A link handed outside - opens without a session while its signature, expiry and record say so (decision 3). */\n routes.get(`/${ATTACHMENT_SHARE_PATH}/:share/:signature`, async (context, next) => {\n const share = await getRepository(AttachmentShare).findOne({ where: { id: context.params.share } as any })\n const now = new Date()\n if (!share || !shareOpens(SECRET, share as any, context.params.signature, now)) {\n context.status = 404\n return\n }\n const row = await getRepository(Attachment).findOne({ where: { id: share.attachmentId, domain: { id: share.domainId } } as any })\n if (!row) {\n context.status = 404\n return\n }\n /* Never kept by anyone: the link can be taken back, and a copy in a cache would outlive that. */\n context.set('Cache-Control', 'private, no-store')\n context.set('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(row.name || row.path)}`)\n await STORAGE.sendFile(context, row.path, next)\n })\n})\n\nprocess.on('bootstrap-module-domain-private-route' as any, (app, routes) => {\n if (isPathBaseDomain) routes.get(`/${getRoutePrefixForDomainType()}/:domain/${ATTACHMENT_PATH}/:attachment`, sendReadable)\n if (requireDomain()) routes.get(`/${ATTACHMENT_PATH}/:attachment`, sendReadable)\n})\n"]}
@@ -2,4 +2,4 @@ import { Attachment } from './attachment';
2
2
  import { AttachmentMutation } from './attachment-mutation';
3
3
  import { AttachmentQuery } from './attachment-query';
4
4
  export declare const entities: (typeof Attachment)[];
5
- export declare const resolvers: (typeof AttachmentMutation | typeof AttachmentQuery)[];
5
+ export declare const resolvers: (typeof AttachmentQuery | typeof AttachmentMutation)[];
@@ -0,0 +1,11 @@
1
+ export declare class AttachmentShareLink {
2
+ id: string;
3
+ url: string;
4
+ expiresAt: Date;
5
+ }
6
+ export declare class AttachmentShareMutation {
7
+ /** Only for someone who may read the attachment - a link never opens more than its maker could. */
8
+ createAttachmentShareLink(attachmentId: string, context: ResolverContext, expiresInHours?: number): Promise<AttachmentShareLink>;
9
+ /** The maker or anyone who may read the attachment takes a link back - it stops opening at once. */
10
+ revokeAttachmentShareLink(id: string, context: ResolverContext): Promise<boolean>;
11
+ }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AttachmentShareMutation = exports.AttachmentShareLink = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const type_graphql_1 = require("type-graphql");
6
+ const env_1 = require("@things-factory/env");
7
+ const shell_1 = require("@things-factory/shell");
8
+ const auth_base_1 = require("@things-factory/auth-base");
9
+ const attachment_access_1 = require("../../attachment-access");
10
+ const attachment_1 = require("../attachment/attachment");
11
+ const attachment_share_1 = require("./attachment-share");
12
+ const attachment_share_rule_1 = require("./attachment-share-rule");
13
+ let AttachmentShareLink = class AttachmentShareLink {
14
+ };
15
+ exports.AttachmentShareLink = AttachmentShareLink;
16
+ tslib_1.__decorate([
17
+ (0, type_graphql_1.Field)({ description: 'The share link record.' }),
18
+ tslib_1.__metadata("design:type", String)
19
+ ], AttachmentShareLink.prototype, "id", void 0);
20
+ tslib_1.__decorate([
21
+ (0, type_graphql_1.Field)({ description: 'The address that opens the attachment, relative to the server.' }),
22
+ tslib_1.__metadata("design:type", String)
23
+ ], AttachmentShareLink.prototype, "url", void 0);
24
+ tslib_1.__decorate([
25
+ (0, type_graphql_1.Field)({ description: 'When the address stops opening.' }),
26
+ tslib_1.__metadata("design:type", Date)
27
+ ], AttachmentShareLink.prototype, "expiresAt", void 0);
28
+ exports.AttachmentShareLink = AttachmentShareLink = tslib_1.__decorate([
29
+ (0, type_graphql_1.ObjectType)({ description: 'A share link made for one attachment.' })
30
+ ], AttachmentShareLink);
31
+ let AttachmentShareMutation = class AttachmentShareMutation {
32
+ /** Only for someone who may read the attachment - a link never opens more than its maker could. */
33
+ async createAttachmentShareLink(attachmentId, context, expiresInHours) {
34
+ const { domain, user, tx } = context.state;
35
+ const attachment = await (0, shell_1.getRepository)(attachment_1.Attachment, tx).findOne({ where: { id: attachmentId, domain: { id: domain.id } } });
36
+ if (!attachment || !(await (0, attachment_access_1.mayRead)(attachment, context))) {
37
+ throw new Error(context.t('error.attachment not found'));
38
+ }
39
+ const defaultHours = Number(env_1.config.get('attachment', {})?.shareLinkTtlHours ?? 168);
40
+ const share = await (0, shell_1.getRepository)(attachment_share_1.AttachmentShare, tx).save({
41
+ domain,
42
+ attachment,
43
+ creator: user,
44
+ expiresAt: (0, attachment_share_rule_1.shareExpiry)(new Date(), defaultHours, expiresInHours)
45
+ });
46
+ return {
47
+ id: share.id,
48
+ url: (0, attachment_share_rule_1.shareUrl)(auth_base_1.SECRET, { id: share.id, attachmentId: attachment.id, domainId: domain.id, expiresAt: share.expiresAt }),
49
+ expiresAt: share.expiresAt
50
+ };
51
+ }
52
+ /** The maker or anyone who may read the attachment takes a link back - it stops opening at once. */
53
+ async revokeAttachmentShareLink(id, context) {
54
+ const { domain, tx } = context.state;
55
+ const share = await (0, shell_1.getRepository)(attachment_share_1.AttachmentShare, tx).findOne({ where: { id, domain: { id: domain.id } }, relations: ['attachment'] });
56
+ if (!share || !(await (0, attachment_access_1.mayRead)(share.attachment, context)))
57
+ return false;
58
+ await (0, shell_1.getRepository)(attachment_share_1.AttachmentShare, tx).update({ id }, { revokedAt: new Date() });
59
+ return true;
60
+ }
61
+ };
62
+ exports.AttachmentShareMutation = AttachmentShareMutation;
63
+ tslib_1.__decorate([
64
+ (0, type_graphql_1.Directive)('@privilege(category: "attachment", privilege: "query", domainOwnerGranted: true)'),
65
+ (0, type_graphql_1.Directive)('@transaction'),
66
+ (0, type_graphql_1.Mutation)(returns => AttachmentShareLink, { description: 'To make an expiring link to an attachment for someone outside the domain.' }),
67
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('attachmentId')),
68
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
69
+ tslib_1.__param(2, (0, type_graphql_1.Arg)('expiresInHours', type => type_graphql_1.Int, { nullable: true })),
70
+ tslib_1.__metadata("design:type", Function),
71
+ tslib_1.__metadata("design:paramtypes", [String, Object, Number]),
72
+ tslib_1.__metadata("design:returntype", Promise)
73
+ ], AttachmentShareMutation.prototype, "createAttachmentShareLink", null);
74
+ tslib_1.__decorate([
75
+ (0, type_graphql_1.Directive)('@privilege(category: "attachment", privilege: "query", domainOwnerGranted: true)'),
76
+ (0, type_graphql_1.Directive)('@transaction'),
77
+ (0, type_graphql_1.Mutation)(returns => Boolean, { description: 'To take back a share link - it stops opening at once.' }),
78
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
79
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
80
+ tslib_1.__metadata("design:type", Function),
81
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
82
+ tslib_1.__metadata("design:returntype", Promise)
83
+ ], AttachmentShareMutation.prototype, "revokeAttachmentShareLink", null);
84
+ exports.AttachmentShareMutation = AttachmentShareMutation = tslib_1.__decorate([
85
+ (0, type_graphql_1.Resolver)(attachment_share_1.AttachmentShare)
86
+ ], AttachmentShareMutation);
87
+ //# sourceMappingURL=attachment-share-mutation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-share-mutation.js","sourceRoot":"","sources":["../../../server/service/attachment-share/attachment-share-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Field, Int, Mutation, ObjectType, Resolver } from 'type-graphql'\n\nimport { config } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport { SECRET } from '@things-factory/auth-base'\n\nimport { mayRead } from '../../attachment-access'\nimport { Attachment } from '../attachment/attachment'\nimport { AttachmentShare } from './attachment-share'\nimport { shareExpiry, shareUrl } from './attachment-share-rule'\n\n@ObjectType({ description: 'A share link made for one attachment.' })\nexport class AttachmentShareLink {\n @Field({ description: 'The share link record.' })\n id: string\n\n @Field({ description: 'The address that opens the attachment, relative to the server.' })\n url: string\n\n @Field({ description: 'When the address stops opening.' })\n expiresAt: Date\n}\n\n@Resolver(AttachmentShare)\nexport class AttachmentShareMutation {\n /** Only for someone who may read the attachment - a link never opens more than its maker could. */\n @Directive('@privilege(category: \"attachment\", privilege: \"query\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => AttachmentShareLink, { description: 'To make an expiring link to an attachment for someone outside the domain.' })\n async createAttachmentShareLink(\n @Arg('attachmentId') attachmentId: string,\n @Ctx() context: ResolverContext,\n @Arg('expiresInHours', type => Int, { nullable: true }) expiresInHours?: number\n ): Promise<AttachmentShareLink> {\n const { domain, user, tx } = context.state\n const attachment = await getRepository(Attachment, tx).findOne({ where: { id: attachmentId, domain: { id: domain.id } } as any })\n if (!attachment || !(await mayRead(attachment, context))) {\n throw new Error(context.t('error.attachment not found'))\n }\n\n const defaultHours = Number(config.get('attachment', {})?.shareLinkTtlHours ?? 168)\n const share = await getRepository(AttachmentShare, tx).save({\n domain,\n attachment,\n creator: user,\n expiresAt: shareExpiry(new Date(), defaultHours, expiresInHours)\n } as Partial<AttachmentShare>)\n\n return {\n id: share.id,\n url: shareUrl(SECRET, { id: share.id, attachmentId: attachment.id, domainId: domain.id, expiresAt: share.expiresAt }),\n expiresAt: share.expiresAt\n }\n }\n\n /** The maker or anyone who may read the attachment takes a link back - it stops opening at once. */\n @Directive('@privilege(category: \"attachment\", privilege: \"query\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Boolean, { description: 'To take back a share link - it stops opening at once.' })\n async revokeAttachmentShareLink(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n const { domain, tx } = context.state\n const share = await getRepository(AttachmentShare, tx).findOne({ where: { id, domain: { id: domain.id } } as any, relations: ['attachment'] })\n if (!share || !(await mayRead(share.attachment, context))) return false\n await getRepository(AttachmentShare, tx).update({ id }, { revokedAt: new Date() })\n return true\n }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ export declare const ATTACHMENT_SHARE_PATH = "attachment-share";
2
+ /** What the signature binds - the link row, its attachment, its domain and its expiry. */
3
+ export type ShareFacts = {
4
+ id: string;
5
+ attachmentId: string;
6
+ domainId: string;
7
+ expiresAt: Date | string;
8
+ };
9
+ export declare function shareSignature(secret: string, share: ShareFacts): string;
10
+ export declare function shareUrl(secret: string, share: ShareFacts): string;
11
+ /** Opens only with the right signature, before its expiry, while not taken back. */
12
+ export declare function shareOpens(secret: string, share: (ShareFacts & {
13
+ revokedAt?: Date | string | null;
14
+ }) | null | undefined, signature: string, now: Date): boolean;
15
+ /** The expiry a new link gets: the caller may shorten the installation's default, never lengthen it. */
16
+ export declare function shareExpiry(now: Date, defaultHours: number, requestedHours?: number | null): Date;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ATTACHMENT_SHARE_PATH = void 0;
4
+ exports.shareSignature = shareSignature;
5
+ exports.shareUrl = shareUrl;
6
+ exports.shareOpens = shareOpens;
7
+ exports.shareExpiry = shareExpiry;
8
+ const crypto_1 = require("crypto");
9
+ /*
10
+ * The share link's signature and its standing - pure, so the rules are tested without a server (ADR-0106 decision 3).
11
+ */
12
+ exports.ATTACHMENT_SHARE_PATH = 'attachment-share';
13
+ function shareSignature(secret, share) {
14
+ const at = new Date(share.expiresAt).getTime();
15
+ return (0, crypto_1.createHmac)('sha256', `attachment-share:${secret}`)
16
+ .update([share.id, share.attachmentId, share.domainId, String(at)].join('.'))
17
+ .digest('base64url');
18
+ }
19
+ function shareUrl(secret, share) {
20
+ return `/${exports.ATTACHMENT_SHARE_PATH}/${encodeURIComponent(share.id)}/${shareSignature(secret, share)}`;
21
+ }
22
+ /** Opens only with the right signature, before its expiry, while not taken back. */
23
+ function shareOpens(secret, share, signature, now) {
24
+ if (!share || !signature)
25
+ return false;
26
+ if (share.revokedAt)
27
+ return false;
28
+ const at = new Date(share.expiresAt).getTime();
29
+ if (!Number.isFinite(at) || at <= now.getTime())
30
+ return false;
31
+ const expected = Buffer.from(shareSignature(secret, share));
32
+ const given = Buffer.from(String(signature));
33
+ return expected.length === given.length && (0, crypto_1.timingSafeEqual)(expected, given);
34
+ }
35
+ /** The expiry a new link gets: the caller may shorten the installation's default, never lengthen it. */
36
+ function shareExpiry(now, defaultHours, requestedHours) {
37
+ const fallback = Number.isFinite(defaultHours) && defaultHours > 0 ? defaultHours : 168;
38
+ const hours = requestedHours && requestedHours > 0 ? Math.min(requestedHours, fallback) : fallback;
39
+ return new Date(now.getTime() + hours * 60 * 60 * 1000);
40
+ }
41
+ //# sourceMappingURL=attachment-share-rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-share-rule.js","sourceRoot":"","sources":["../../../server/service/attachment-share/attachment-share-rule.ts"],"names":[],"mappings":";;;AAWA,wCAKC;AAED,4BAEC;AAGD,gCAaC;AAGD,kCAIC;AA3CD,mCAAoD;AAEpD;;GAEG;AAEU,QAAA,qBAAqB,GAAG,kBAAkB,CAAA;AAKvD,SAAgB,cAAc,CAAC,MAAc,EAAE,KAAiB;IAC9D,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAA;IAC9C,OAAO,IAAA,mBAAU,EAAC,QAAQ,EAAE,oBAAoB,MAAM,EAAE,CAAC;SACtD,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;SAC5E,MAAM,CAAC,WAAW,CAAC,CAAA;AACxB,CAAC;AAED,SAAgB,QAAQ,CAAC,MAAc,EAAE,KAAiB;IACxD,OAAO,IAAI,6BAAqB,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAA;AACrG,CAAC;AAED,oFAAoF;AACpF,SAAgB,UAAU,CACxB,MAAc,EACd,KAA6E,EAC7E,SAAiB,EACjB,GAAS;IAET,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAA;IACtC,IAAI,KAAK,CAAC,SAAS;QAAE,OAAO,KAAK,CAAA;IACjC,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAA;IAC9C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,OAAO,EAAE;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;IAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;IAC5C,OAAO,QAAQ,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,IAAA,wBAAe,EAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;AAC7E,CAAC;AAED,wGAAwG;AACxG,SAAgB,WAAW,CAAC,GAAS,EAAE,YAAoB,EAAE,cAA8B;IACzF,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAA;IACvF,MAAM,KAAK,GAAG,cAAc,IAAI,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAClG,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;AACzD,CAAC","sourcesContent":["import { createHmac, timingSafeEqual } from 'crypto'\n\n/*\n * The share link's signature and its standing - pure, so the rules are tested without a server (ADR-0106 decision 3).\n */\n\nexport const ATTACHMENT_SHARE_PATH = 'attachment-share'\n\n/** What the signature binds - the link row, its attachment, its domain and its expiry. */\nexport type ShareFacts = { id: string; attachmentId: string; domainId: string; expiresAt: Date | string }\n\nexport function shareSignature(secret: string, share: ShareFacts): string {\n const at = new Date(share.expiresAt).getTime()\n return createHmac('sha256', `attachment-share:${secret}`)\n .update([share.id, share.attachmentId, share.domainId, String(at)].join('.'))\n .digest('base64url')\n}\n\nexport function shareUrl(secret: string, share: ShareFacts): string {\n return `/${ATTACHMENT_SHARE_PATH}/${encodeURIComponent(share.id)}/${shareSignature(secret, share)}`\n}\n\n/** Opens only with the right signature, before its expiry, while not taken back. */\nexport function shareOpens(\n secret: string,\n share: (ShareFacts & { revokedAt?: Date | string | null }) | null | undefined,\n signature: string,\n now: Date\n): boolean {\n if (!share || !signature) return false\n if (share.revokedAt) return false\n const at = new Date(share.expiresAt).getTime()\n if (!Number.isFinite(at) || at <= now.getTime()) return false\n const expected = Buffer.from(shareSignature(secret, share))\n const given = Buffer.from(String(signature))\n return expected.length === given.length && timingSafeEqual(expected, given)\n}\n\n/** The expiry a new link gets: the caller may shorten the installation's default, never lengthen it. */\nexport function shareExpiry(now: Date, defaultHours: number, requestedHours?: number | null): Date {\n const fallback = Number.isFinite(defaultHours) && defaultHours > 0 ? defaultHours : 168\n const hours = requestedHours && requestedHours > 0 ? Math.min(requestedHours, fallback) : fallback\n return new Date(now.getTime() + hours * 60 * 60 * 1000)\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { Domain } from '@things-factory/shell';
2
+ import { User } from '@things-factory/auth-base';
3
+ import { Attachment } from '../attachment/attachment';
4
+ export declare class AttachmentShare {
5
+ readonly id: string;
6
+ domain: Domain;
7
+ domainId: string;
8
+ attachment: Attachment;
9
+ attachmentId: string;
10
+ creator?: User;
11
+ creatorId?: string;
12
+ expiresAt: Date;
13
+ revokedAt?: Date;
14
+ createdAt: Date;
15
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AttachmentShare = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const typeorm_1 = require("typeorm");
6
+ const type_graphql_1 = require("type-graphql");
7
+ const shell_1 = require("@things-factory/shell");
8
+ const auth_base_1 = require("@things-factory/auth-base");
9
+ const attachment_1 = require("../attachment/attachment");
10
+ /*
11
+ * A link to one attachment handed outside the domain - Label Studio, a mail, a person told by hand (ADR-0106 decision 3).
12
+ *
13
+ * The row is the record and the off switch: who made it, for which attachment, until when, and whether it was taken
14
+ * back. The URL carries this row's id and an HMAC over (id, attachment, domain, expiry); the attachment's own path is
15
+ * not in it.
16
+ */
17
+ let AttachmentShare = class AttachmentShare {
18
+ };
19
+ exports.AttachmentShare = AttachmentShare;
20
+ tslib_1.__decorate([
21
+ (0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
22
+ (0, type_graphql_1.Field)(type => type_graphql_1.ID, { description: 'Unique identifier of the share link.' }),
23
+ tslib_1.__metadata("design:type", String)
24
+ ], AttachmentShare.prototype, "id", void 0);
25
+ tslib_1.__decorate([
26
+ (0, typeorm_1.ManyToOne)(type => shell_1.Domain),
27
+ (0, type_graphql_1.Field)(type => shell_1.Domain, { description: 'The domain the attachment belongs to.' }),
28
+ tslib_1.__metadata("design:type", shell_1.Domain)
29
+ ], AttachmentShare.prototype, "domain", void 0);
30
+ tslib_1.__decorate([
31
+ (0, typeorm_1.RelationId)((share) => share.domain),
32
+ tslib_1.__metadata("design:type", String)
33
+ ], AttachmentShare.prototype, "domainId", void 0);
34
+ tslib_1.__decorate([
35
+ (0, typeorm_1.ManyToOne)(type => attachment_1.Attachment, { onDelete: 'CASCADE' }),
36
+ (0, type_graphql_1.Field)(type => attachment_1.Attachment, { description: 'The attachment the link opens.' }),
37
+ tslib_1.__metadata("design:type", attachment_1.Attachment)
38
+ ], AttachmentShare.prototype, "attachment", void 0);
39
+ tslib_1.__decorate([
40
+ (0, typeorm_1.RelationId)((share) => share.attachment),
41
+ tslib_1.__metadata("design:type", String)
42
+ ], AttachmentShare.prototype, "attachmentId", void 0);
43
+ tslib_1.__decorate([
44
+ (0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
45
+ (0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true, description: 'Who made the link.' }),
46
+ tslib_1.__metadata("design:type", auth_base_1.User)
47
+ ], AttachmentShare.prototype, "creator", void 0);
48
+ tslib_1.__decorate([
49
+ (0, typeorm_1.RelationId)((share) => share.creator),
50
+ tslib_1.__metadata("design:type", String)
51
+ ], AttachmentShare.prototype, "creatorId", void 0);
52
+ tslib_1.__decorate([
53
+ (0, typeorm_1.Column)(),
54
+ (0, type_graphql_1.Field)({ description: 'When the link stops opening.' }),
55
+ tslib_1.__metadata("design:type", Date)
56
+ ], AttachmentShare.prototype, "expiresAt", void 0);
57
+ tslib_1.__decorate([
58
+ (0, typeorm_1.Column)({ nullable: true }),
59
+ (0, type_graphql_1.Field)({ nullable: true, description: 'When the link was taken back, if it was.' }),
60
+ tslib_1.__metadata("design:type", Date)
61
+ ], AttachmentShare.prototype, "revokedAt", void 0);
62
+ tslib_1.__decorate([
63
+ (0, typeorm_1.CreateDateColumn)(),
64
+ (0, type_graphql_1.Field)({ description: 'When the link was made.' }),
65
+ tslib_1.__metadata("design:type", Date)
66
+ ], AttachmentShare.prototype, "createdAt", void 0);
67
+ exports.AttachmentShare = AttachmentShare = tslib_1.__decorate([
68
+ (0, typeorm_1.Entity)(),
69
+ (0, typeorm_1.Index)('ix_attachment_share_0', (share) => [share.domain, share.attachment]),
70
+ (0, type_graphql_1.ObjectType)({ description: 'A signed, expiring link to one attachment, for someone outside the domain.' })
71
+ ], AttachmentShare);
72
+ //# sourceMappingURL=attachment-share.js.map